easycoder 251103.2__py2.py3-none-any.whl → 251104.2__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.

Potentially problematic release.


This version of easycoder might be problematic. Click here for more details.

easycoder/__init__.py CHANGED
@@ -12,4 +12,4 @@ from .ec_pyside import *
12
12
  from .ec_timestamp import *
13
13
  from .ec_value import *
14
14
 
15
- __version__ = "251103.2"
15
+ __version__ = "251104.2"
easycoder/ec_classes.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
 
3
- class FatalError:
3
+ class FatalError(BaseException):
4
4
  def __init__(self, compiler, message):
5
5
  compiler.showWarnings()
6
6
  lino = compiler.tokens[compiler.index].lino
@@ -58,4 +58,9 @@ class Token:
58
58
  self.token = token
59
59
 
60
60
  class Object():
61
- pass
61
+ """Dynamic object that allows arbitrary attribute assignment"""
62
+ def __setattr__(self, name: str, value) -> None:
63
+ self.__dict__[name] = value
64
+
65
+ def __getattr__(self, name: str):
66
+ return self.__dict__.get(name)
easycoder/ec_compiler.py CHANGED
@@ -1,4 +1,4 @@
1
- from .ec_classes import Token, FatalError
1
+ from .ec_classes import FatalError
2
2
  from .ec_value import Value
3
3
  from .ec_condition import Condition
4
4
 
@@ -18,9 +18,11 @@ class Compiler:
18
18
  self.debugCompile = False
19
19
  self.valueTypes = {}
20
20
 
21
- def getPC(self):
21
+ # Get the current code size. Used during compilation
22
+ def getCodeSize(self):
22
23
  return len(self.program.code)
23
24
 
25
+ # Get the current index (the program counter)
24
26
  def getIndex(self):
25
27
  return self.index
26
28
 
@@ -39,6 +41,7 @@ class Compiler:
39
41
  self.index += 1
40
42
  return self.getToken()
41
43
 
44
+ # Peek ahead to see the next token without advancing the index
42
45
  def peek(self):
43
46
  try:
44
47
  return self.tokens[self.index + 1].token
@@ -68,22 +71,24 @@ class Compiler:
68
71
  self.index += 1
69
72
  return self.condition.compileCondition()
70
73
 
74
+ # Test if the current token has a specified value
71
75
  def tokenIs(self, value):
72
76
  return self.getToken() == value
73
77
 
78
+ # Test if the next token has the specified value
74
79
  def nextIs(self, value):
75
80
  return self.nextToken() == value
76
81
 
82
+ # Get the command at a given pc in the code list
77
83
  def getCommandAt(self, pc):
78
84
  return self.program.code[pc]
79
85
 
80
86
  # Add a command to the code list
81
87
  def addCommand(self, command):
82
- if len(self.code) == 0:
83
- if self.program.usingGraphics:
84
- pass
88
+ command['bp'] = False
85
89
  self.code.append(command)
86
90
 
91
+ # Test if the current token is a symbol
87
92
  def isSymbol(self):
88
93
  token = self.getToken()
89
94
  try:
@@ -92,10 +97,12 @@ class Compiler:
92
97
  return False
93
98
  return True
94
99
 
100
+ # Test if the next token is a symbol
95
101
  def nextIsSymbol(self):
96
102
  self.next()
97
103
  return self.isSymbol()
98
104
 
105
+ # Skip the next token if it matches the value given
99
106
  def skip(self, token):
100
107
  next = self.peek()
101
108
  if type(token) == list:
@@ -105,21 +112,26 @@ class Compiler:
105
112
  return
106
113
  elif next == token: self.nextToken()
107
114
 
115
+ # Rewind to a given position in the code list
108
116
  def rewindTo(self, index):
109
117
  self.index = index
110
118
 
119
+ # Get source line number containing the current token
111
120
  def getLino(self):
112
121
  if self.index >= len(self.tokens):
113
122
  return 0
114
123
  return self.tokens[self.index].lino
115
124
 
125
+ # Issue a warning
116
126
  def warning(self, message):
117
127
  self.warnings.append(f'Warning at line {self.getLino() + 1} of {self.program.name}: {message}')
118
128
 
129
+ # Print all warnings
119
130
  def showWarnings(self):
120
131
  for warning in self.warnings:
121
132
  print(warning)
122
133
 
134
+ # Get the symbol record for the current token (assumes it is a symbol name)
123
135
  def getSymbolRecord(self):
124
136
  token = self.getToken()
125
137
  if not token in self.symbols:
@@ -131,18 +143,23 @@ class Compiler:
131
143
  symbolRecord['used'] = True
132
144
  return symbolRecord
133
145
 
146
+ # Add a value type
134
147
  def addValueType(self):
135
148
  self.valueTypes[self.getToken()] = True
136
149
 
150
+ # Test if a given value is in the value types list
137
151
  def hasValue(self, type):
138
152
  return type in self.valueTypes
139
153
 
154
+ # Compile a program label (a symbol ending with ':')
140
155
  def compileLabel(self, command):
141
156
  return self.compileSymbol(command, self.getToken())
142
157
 
158
+ # Compile a variable
143
159
  def compileVariable(self, command, extra=None):
144
160
  return self.compileSymbol(command, self.nextToken(), extra)
145
161
 
162
+ # Compile a symbol
146
163
  def compileSymbol(self, command, name, extra=None):
147
164
  try:
148
165
  v = self.symbols[name]
@@ -151,7 +168,7 @@ class Compiler:
151
168
  if v:
152
169
  FatalError(self, f'Duplicate symbol name "{name}"')
153
170
  return False
154
- self.symbols[name] = self.getPC()
171
+ self.symbols[name] = self.getCodeSize()
155
172
  command['program'] = self.program
156
173
  command['type'] = 'symbol'
157
174
  command['name'] = name
@@ -174,6 +191,10 @@ class Compiler:
174
191
  # print(f'Compile {token}')
175
192
  if not token:
176
193
  return False
194
+ if len(self.code) == 0:
195
+ if self.program.parent == None and self.program.usingGraphics:
196
+ cmd = {'domain': 'graphics', 'keyword': 'init', 'debug': False}
197
+ self.code.append(cmd)
177
198
  mark = self.getIndex()
178
199
  for domain in self.program.getDomains():
179
200
  handler = domain.keywordHandler(token)
@@ -198,7 +219,7 @@ class Compiler:
198
219
  keyword = self.getToken()
199
220
  if not keyword:
200
221
  return False
201
- # print(f'Compile keyword "{keyword}"')
222
+ # print(f'Compile keyword "{keyword}"')
202
223
  if keyword.endswith(':'):
203
224
  command = {}
204
225
  command['domain'] = None
@@ -224,8 +245,10 @@ class Compiler:
224
245
  else:
225
246
  return False
226
247
 
248
+ # Compile fom the current location, stopping on any of a list of tokens
227
249
  def compileFromHere(self, stopOn):
228
250
  return self.compileFrom(self.getIndex(), stopOn)
229
251
 
252
+ # Compile from the start of the script
230
253
  def compileFromStart(self):
231
254
  return self.compileFrom(0, [])
easycoder/ec_core.py CHANGED
@@ -31,13 +31,13 @@ class Core(Handler):
31
31
  cmd['keyword'] = 'gotoPC'
32
32
  cmd['goto'] = 0
33
33
  cmd['debug'] = False
34
- skip = self.getPC()
34
+ skip = self.getCodeSize()
35
35
  self.add(cmd)
36
36
  # Process the 'or'
37
- self.getCommandAt(orHere)['or'] = self.getPC()
37
+ self.getCommandAt(orHere)['or'] = self.getCodeSize()
38
38
  self.compileOne()
39
39
  # Fixup the skip
40
- self.getCommandAt(skip)['goto'] = self.getPC()
40
+ self.getCommandAt(skip)['goto'] = self.getCodeSize()
41
41
 
42
42
  #############################################################################
43
43
  # Keyword handlers
@@ -468,7 +468,7 @@ class Core(Handler):
468
468
  if url != None:
469
469
  command['url'] = url
470
470
  command['or'] = None
471
- get = self.getPC()
471
+ get = self.getCodeSize()
472
472
  if self.peek() == 'timeout':
473
473
  self.nextToken()
474
474
  command['timeout'] = self.nextValue()
@@ -556,7 +556,7 @@ class Core(Handler):
556
556
  command['condition'] = self.nextCondition()
557
557
  self.add(command)
558
558
  self.nextToken()
559
- pcElse = self.getPC()
559
+ pcElse = self.getCodeSize()
560
560
  cmd = {}
561
561
  cmd['lino'] = command['lino']
562
562
  cmd['domain'] = 'core'
@@ -569,7 +569,7 @@ class Core(Handler):
569
569
  if self.peek() == 'else':
570
570
  self.nextToken()
571
571
  # Add a 'goto' to skip the 'else'
572
- pcNext = self.getPC()
572
+ pcNext = self.getCodeSize()
573
573
  cmd = {}
574
574
  cmd['lino'] = command['lino']
575
575
  cmd['domain'] = 'core'
@@ -578,15 +578,15 @@ class Core(Handler):
578
578
  cmd['debug'] = False
579
579
  self.add(cmd)
580
580
  # Fixup the link to the 'else' branch
581
- self.getCommandAt(pcElse)['goto'] = self.getPC()
581
+ self.getCommandAt(pcElse)['goto'] = self.getCodeSize()
582
582
  # Process the 'else' branch
583
583
  self.nextToken()
584
584
  self.compileOne()
585
585
  # Fixup the pcNext 'goto'
586
- self.getCommandAt(pcNext)['goto'] = self.getPC()
586
+ self.getCommandAt(pcNext)['goto'] = self.getCodeSize()
587
587
  else:
588
588
  # We're already at the next command
589
- self.getCommandAt(pcElse)['goto'] = self.getPC()
589
+ self.getCommandAt(pcElse)['goto'] = self.getCodeSize()
590
590
  return True
591
591
 
592
592
  def r_if(self, command):
@@ -605,7 +605,7 @@ class Core(Handler):
605
605
  name = self.nextToken()
606
606
  item = [keyword, name]
607
607
  imports.append(item)
608
- self.symbols[name] = self.getPC()
608
+ self.symbols[name] = self.getCodeSize()
609
609
  variable = {}
610
610
  variable['domain'] = None
611
611
  variable['name'] = name
@@ -747,7 +747,7 @@ class Core(Handler):
747
747
  else:
748
748
  command['file'] = self.getValue()
749
749
  command['or'] = None
750
- load = self.getPC()
750
+ load = self.getCodeSize()
751
751
  self.processOr(command, load)
752
752
  return True
753
753
  else:
@@ -916,7 +916,7 @@ class Core(Handler):
916
916
  cmd['debug'] = False
917
917
  self.add(cmd)
918
918
  # Fixup the link
919
- command['goto'] = self.getPC()
919
+ command['goto'] = self.getCodeSize()
920
920
  return True
921
921
  return False
922
922
 
@@ -1007,7 +1007,7 @@ class Core(Handler):
1007
1007
  else:
1008
1008
  command['result'] = None
1009
1009
  command['or'] = None
1010
- post = self.getPC()
1010
+ post = self.getCodeSize()
1011
1011
  self.processOr(command, post)
1012
1012
  return True
1013
1013
 
@@ -1106,7 +1106,7 @@ class Core(Handler):
1106
1106
  FatalError(self.compiler, f'Symbol {symbolRecord["name"]} is not a value holder')
1107
1107
  else:
1108
1108
  command['or'] = None
1109
- self.processOr(command, self.getPC())
1109
+ self.processOr(command, self.getCodeSize())
1110
1110
  return True
1111
1111
  else:
1112
1112
  FatalError(self.compiler, f'Symbol {self.getToken()} is not a variable')
@@ -1274,7 +1274,7 @@ class Core(Handler):
1274
1274
  else:
1275
1275
  command['file'] = self.getValue()
1276
1276
  command['or'] = None
1277
- save = self.getPC()
1277
+ save = self.getCodeSize()
1278
1278
  self.processOr(command, save)
1279
1279
  return True
1280
1280
 
@@ -1772,7 +1772,7 @@ class Core(Handler):
1772
1772
  else:
1773
1773
  token = self.nextToken()
1774
1774
  if token in ['graphics', 'debugger']:
1775
- if not hasattr(self.program, 'usingGraphics'):
1775
+ if not self.program.usingGraphics:
1776
1776
  print('Loading graphics module')
1777
1777
  from .ec_pyside import Graphics
1778
1778
  self.program.graphics = Graphics
@@ -1823,10 +1823,10 @@ class Core(Handler):
1823
1823
  return None
1824
1824
  # token = self.getToken()
1825
1825
  command['condition'] = code
1826
- test = self.getPC()
1826
+ test = self.getCodeSize()
1827
1827
  self.add(command)
1828
1828
  # Set up a goto for when the test fails
1829
- fail = self.getPC()
1829
+ fail = self.getCodeSize()
1830
1830
  cmd = {}
1831
1831
  cmd['lino'] = command['lino']
1832
1832
  cmd['domain'] = 'core'
@@ -1847,7 +1847,7 @@ class Core(Handler):
1847
1847
  cmd['debug'] = False
1848
1848
  self.add(cmd)
1849
1849
  # Fixup the 'goto' on completion
1850
- self.getCommandAt(fail)['goto'] = self.getPC()
1850
+ self.getCommandAt(fail)['goto'] = self.getCodeSize()
1851
1851
  return True
1852
1852
 
1853
1853
  def r_while(self, command):
easycoder/ec_debug.py ADDED
@@ -0,0 +1,464 @@
1
+ import sys, os, json, html
2
+ from PySide6.QtWidgets import (
3
+ QMainWindow,
4
+ QWidget,
5
+ QFrame,
6
+ QHBoxLayout,
7
+ QVBoxLayout,
8
+ QLabel,
9
+ QSplitter,
10
+ QFileDialog,
11
+ QMessageBox,
12
+ QScrollArea,
13
+ QSizePolicy,
14
+ QToolBar
15
+ )
16
+ from PySide6.QtGui import QAction, QKeySequence, QTextCursor
17
+ from PySide6.QtCore import Qt, QTimer, QProcess
18
+ from typing import Any
19
+
20
+ class Object():
21
+ """Dynamic object that allows arbitrary attribute assignment"""
22
+ def __setattr__(self, name: str, value: Any) -> None:
23
+ self.__dict__[name] = value
24
+
25
+ def __getattr__(self, name: str) -> Any:
26
+ return self.__dict__.get(name)
27
+
28
+ class Debugger(QMainWindow):
29
+
30
+ ###########################################################################
31
+ # The left-hand column of the main window
32
+ class MainLeftColumn(QWidget):
33
+ def __init__(self, parent=None):
34
+ super().__init__(parent)
35
+ layout = QVBoxLayout(self)
36
+ layout.addWidget(QLabel("Left column"))
37
+ layout.addStretch()
38
+
39
+ ###########################################################################
40
+ # The right-hand column of the main window
41
+ class MainRightColumn(QWidget):
42
+ scroll: QScrollArea
43
+ layout: QHBoxLayout # type: ignore[assignment]
44
+ blob: QLabel
45
+
46
+ def __init__(self, parent=None):
47
+ super().__init__(parent)
48
+
49
+ # Create a scroll area - its content widget holds the lines
50
+ self.scroll = QScrollArea(self)
51
+ self.scroll.setWidgetResizable(True)
52
+
53
+ # Ensure this widget and the scroll area expand to fill available space
54
+ self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
55
+ self.scroll.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
56
+
57
+ self.content = QWidget()
58
+ # let the content expand horizontally but have flexible height
59
+ self.content.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
60
+
61
+ self.inner_layout = QVBoxLayout(self.content)
62
+ # spacing and small top/bottom margins to separate lines
63
+ self.inner_layout.setSpacing(0)
64
+ self.inner_layout.setContentsMargins(0, 0, 0, 0)
65
+
66
+ self.scroll.setWidget(self.content)
67
+
68
+ # outer layout for this widget contains only the scroll area
69
+ main_layout = QVBoxLayout(self)
70
+ main_layout.setContentsMargins(0, 0, 0, 0)
71
+ main_layout.addWidget(self.scroll)
72
+ # ensure the scroll area gets the stretch so it fills the parent
73
+ main_layout.setStretch(0, 1)
74
+
75
+ #######################################################################
76
+ # Add a line to the right-hand column
77
+ def addLine(self, spec):
78
+ class Label(QLabel):
79
+ def __init__(self, text, fixed_width=None, align=Qt.AlignmentFlag.AlignLeft, on_click=spec.onClick):
80
+ super().__init__()
81
+ self.setText(text)
82
+ # remove QLabel's internal margins/padding to reduce top/bottom space
83
+ self.setMargin(0)
84
+ self.setContentsMargins(0, 0, 0, 0)
85
+ self.setStyleSheet("padding:0px; margin:0px; font-family: mono")
86
+ fm = self.fontMetrics()
87
+ # set a compact fixed height based on font metrics
88
+ self.setFixedHeight(fm.height())
89
+ # optional fixed width (used for the lino column)
90
+ if fixed_width is not None:
91
+ self.setFixedWidth(fixed_width)
92
+ # align horizontally (keep vertically centered)
93
+ self.setAlignment(align | Qt.AlignmentFlag.AlignVCenter)
94
+ # optional click callback
95
+ self._on_click = on_click
96
+
97
+ def mousePressEvent(self, event):
98
+ if self._on_click:
99
+ try:
100
+ self._on_click()
101
+ except Exception:
102
+ pass
103
+ super().mousePressEvent(event)
104
+
105
+ spec.label = self
106
+ panel = QWidget()
107
+ # ensure the panel itself has no margins
108
+ try:
109
+ panel.setContentsMargins(0, 0, 0, 0)
110
+ except Exception:
111
+ pass
112
+ # tidy layout: remove spacing/margins so lines sit flush
113
+ layout = QHBoxLayout(panel)
114
+ layout.setSpacing(0)
115
+ layout.setContentsMargins(0, 0, 0, 0)
116
+ self.layout: QHBoxLayout = layout # type: ignore
117
+ # make panel take minimal vertical space
118
+ panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
119
+ # compute width to fit a 4-digit line number using this widget's font
120
+ fm_main = self.fontMetrics()
121
+ width_4 = fm_main.horizontalAdvance('0000') + 8
122
+
123
+ # create the red blob (always present). We'll toggle its opacity
124
+ # by changing the stylesheet (rgba alpha 255/0). Do NOT store it
125
+ # on the MainRightColumn instance — keep it per-line.
126
+ blob = QLabel()
127
+ blob_size = 10
128
+ blob.setFixedSize(blob_size, blob_size)
129
+
130
+ def set_blob_visible(widget, visible):
131
+ alpha = 255 if visible else 0
132
+ widget.setStyleSheet(f"background-color: rgba(255,0,0,{alpha}); border-radius: {blob_size//2}px; margin:0px; padding:0px;")
133
+ widget._blob_visible = visible
134
+ # force repaint
135
+ widget.update()
136
+
137
+ # attach methods to this blob so callers can toggle it via spec.label
138
+ blob.showBlob = lambda: set_blob_visible(blob, True) # type: ignore[attr-defined]
139
+ blob.hideBlob = lambda: set_blob_visible(blob, False) # type: ignore[attr-defined]
140
+
141
+ # initialize according to spec flag
142
+ if spec.bp:
143
+ blob.showBlob() # type: ignore[attr-defined]
144
+ else:
145
+ blob.hideBlob() # type: ignore[attr-defined]
146
+
147
+ # expose the blob to the outside via spec['label'] so onClick can call showBlob/hideBlob
148
+ spec.label = blob
149
+
150
+ # create the line-number label; clicking it reports back to the caller
151
+ lino_label = Label(str(spec.lino+1), fixed_width=width_4, align=Qt.AlignmentFlag.AlignRight,
152
+ on_click=lambda: spec.onClick(spec.lino))
153
+ lino_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
154
+ # create the text label for the line itself
155
+ text_label = Label(spec.line, fixed_width=None, align=Qt.AlignmentFlag.AlignLeft)
156
+ text_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
157
+ layout.addWidget(lino_label)
158
+ layout.addSpacing(10)
159
+ layout.addWidget(blob, 0, Qt.AlignmentFlag.AlignVCenter)
160
+ layout.addSpacing(3)
161
+ layout.addWidget(text_label)
162
+ self.inner_layout.addWidget(panel)
163
+ return panel
164
+
165
+ def showBlob(self):
166
+ self.blob.setStyleSheet("background-color: red; border-radius: 5px; margin:0px; padding:0px;")
167
+
168
+ def hideBlob(self):
169
+ self.blob.setStyleSheet("background-color: none; border-radius: 5px; margin:0px; padding:0px;")
170
+
171
+ def addStretch(self):
172
+ self.layout.addStretch()
173
+
174
+ ###########################################################################
175
+ # Main debugger class initializer
176
+ def __init__(self, program, width=800, height=600, ratio=0.2):
177
+ super().__init__()
178
+ self.program = program
179
+ self.setWindowTitle("EasyCoder Debugger")
180
+ self.setMinimumSize(width, height)
181
+ self.stopped = True
182
+
183
+ # try to load saved geometry from ~/.ecdebug.conf
184
+ cfg_path = os.path.join(os.path.expanduser("~"), ".ecdebug.conf")
185
+ initial_width = width
186
+ # default console height (pixels) if not stored in cfg
187
+ console_height = 150
188
+ try:
189
+ if os.path.exists(cfg_path):
190
+ with open(cfg_path, "r", encoding="utf-8") as f:
191
+ cfg = json.load(f)
192
+ x = int(cfg.get("x", 0))
193
+ y = int(cfg.get("y", 0))
194
+ w = int(cfg.get("width", width))
195
+ h = int(cfg.get("height", height))
196
+ ratio =float(cfg.get("ratio", ratio))
197
+ # load console height if present
198
+ console_height = int(cfg.get("console_height", console_height))
199
+ # Apply loaded geometry
200
+ self.setGeometry(x, y, w, h)
201
+ initial_width = w
202
+ except Exception:
203
+ # ignore errors and continue with defaults
204
+ initial_width = width
205
+
206
+ # process handle for running scripts
207
+ self._proc = None
208
+ # in-process Program instance and writer
209
+ self._program = None
210
+ self._writer = None
211
+ self._orig_stdout = None
212
+ self._orig_stderr = None
213
+ self._flush_timer = None
214
+
215
+ # Keep a ratio so proportions are preserved when window is resized
216
+ self.ratio = ratio
217
+
218
+ # Central horizontal splitter (left/right)
219
+ self.hsplitter = QSplitter(Qt.Orientation.Horizontal, self)
220
+ self.hsplitter.setHandleWidth(8)
221
+ self.hsplitter.splitterMoved.connect(self.on_splitter_moved)
222
+
223
+ # Left pane
224
+ left = QFrame()
225
+ left.setFrameShape(QFrame.Shape.StyledPanel)
226
+ left_layout = QVBoxLayout(left)
227
+ left_layout.setContentsMargins(8, 8, 8, 8)
228
+ self.leftColumn = self.MainLeftColumn()
229
+ left_layout.addWidget(self.leftColumn)
230
+ left_layout.addStretch()
231
+
232
+ # Right pane
233
+ right = QFrame()
234
+ right.setFrameShape(QFrame.Shape.StyledPanel)
235
+ right_layout = QVBoxLayout(right)
236
+ right_layout.setContentsMargins(8, 8, 8, 8)
237
+ self.rightColumn = self.MainRightColumn()
238
+ # Give the rightColumn a stretch factor so its scroll area fills the vertical space
239
+ right_layout.addWidget(self.rightColumn, 1)
240
+
241
+ # Add panes to horizontal splitter
242
+ self.hsplitter.addWidget(left)
243
+ self.hsplitter.addWidget(right)
244
+
245
+ # Initial sizes (proportional) for horizontal splitter
246
+ total = initial_width
247
+ self.hsplitter.setSizes([int(self.ratio * total), int((1 - self.ratio) * total)])
248
+
249
+ # Create a vertical splitter so we can add a resizable console panel at the bottom
250
+ self.vsplitter = QSplitter(Qt.Orientation.Vertical, self)
251
+ self.vsplitter.setHandleWidth(6)
252
+ # top: the existing horizontal splitter
253
+ self.vsplitter.addWidget(self.hsplitter)
254
+
255
+ # bottom: console panel
256
+ console_frame = QFrame()
257
+ console_frame.setFrameShape(QFrame.Shape.StyledPanel)
258
+ console_layout = QVBoxLayout(console_frame)
259
+ console_layout.setContentsMargins(4, 4, 4, 4)
260
+ # simple read-only text console for script output and messages
261
+ from PySide6.QtWidgets import QTextEdit
262
+ self.console = QTextEdit()
263
+ self.console.setReadOnly(True)
264
+ console_layout.addWidget(self.console)
265
+ self.vsplitter.addWidget(console_frame)
266
+
267
+ # Set initial vertical sizes: prefer saved console_height if available
268
+ try:
269
+ total_h = int(h) if 'h' in locals() else max(300, self.height())
270
+ ch = max(50, min(total_h - 50, console_height))
271
+ self.vsplitter.setSizes([int(total_h - ch), int(ch)])
272
+ except Exception:
273
+ pass
274
+
275
+ # Use the vertical splitter as the central widget
276
+ self.setCentralWidget(self.vsplitter)
277
+ self.parse(program.script.lines)
278
+ self.show()
279
+
280
+ def on_splitter_moved(self, pos, index):
281
+ # Update stored ratio when user drags the splitter
282
+ left_width = self.hsplitter.widget(0).width()
283
+ total = max(1, sum(w.width() for w in (self.hsplitter.widget(0), self.hsplitter.widget(1))))
284
+ self.ratio = left_width / total
285
+
286
+ def resizeEvent(self, event):
287
+ # Preserve the proportional widths when the window is resized
288
+ total_width = max(1, self.width())
289
+ left_w = max(0, int(self.ratio * total_width))
290
+ right_w = max(0, total_width - left_w)
291
+ self.hsplitter.setSizes([left_w, right_w])
292
+ super().resizeEvent(event)
293
+
294
+ ###########################################################################
295
+ # Parse a script into the right-hand column
296
+ def parse(self, script):
297
+ self.scriptLines = []
298
+ # Clear existing lines from the right column layout
299
+ layout = self.rightColumn.inner_layout
300
+ while layout.count():
301
+ item = layout.takeAt(0)
302
+ widget = item.widget()
303
+ if widget:
304
+ widget.deleteLater()
305
+
306
+ # Parse and add new lines
307
+ lino = 0
308
+ for line in script:
309
+ if len(line) > 0:
310
+ line = line.replace("\t", " ")
311
+ line = self.coloriseLine(line, lino)
312
+ else:
313
+ # still need to call coloriseLine to keep token list in sync
314
+ self.coloriseLine(line, lino)
315
+ lineSpec = Object()
316
+ lineSpec.lino = lino
317
+ lineSpec.line = line
318
+ lineSpec.bp = False
319
+ lineSpec.onClick = self.onClickLino
320
+ lino += 1
321
+ self.scriptLines.append(lineSpec)
322
+ lineSpec.panel = self.rightColumn.addLine(lineSpec)
323
+ self.rightColumn.addStretch()
324
+
325
+ ###########################################################################
326
+ # Colorise a line of script for HTML display
327
+ def coloriseLine(self, line, lino=None):
328
+ output = ''
329
+
330
+ # Preserve leading spaces (render as   except the first)
331
+ if len(line) > 0 and line[0] == ' ':
332
+ output += '<span>'
333
+ n = 0
334
+ while n < len(line) and line[n] == ' ': n += 1
335
+ output += '&nbsp;' * (n - 1)
336
+ output += '</span>'
337
+
338
+ # Find the first unquoted ! (not inside backticks)
339
+ comment_start = None
340
+ in_backtick = False
341
+ for idx, c in enumerate(line):
342
+ if c == '`':
343
+ in_backtick = not in_backtick
344
+ elif c == '!' and not in_backtick:
345
+ comment_start = idx
346
+ break
347
+
348
+ if comment_start is not None:
349
+ code_part = line[:comment_start]
350
+ comment_part = line[comment_start:]
351
+ else:
352
+ code_part = line
353
+ comment_part = None
354
+
355
+ # Tokenize code_part as before (respecting backticks)
356
+ tokens = []
357
+ i = 0
358
+ L = len(code_part)
359
+ while i < L:
360
+ if code_part[i].isspace():
361
+ i += 1
362
+ continue
363
+ if code_part[i] == '`':
364
+ j = code_part.find('`', i + 1)
365
+ if j == -1:
366
+ tokens.append(code_part[i:])
367
+ break
368
+ else:
369
+ tokens.append(code_part[i:j+1])
370
+ i = j + 1
371
+ else:
372
+ j = i
373
+ while j < L and not code_part[j].isspace():
374
+ j += 1
375
+ tokens.append(code_part[i:j])
376
+ i = j
377
+
378
+ # Colour code tokens and generate a list of elements
379
+ for token in tokens:
380
+ if token == '':
381
+ continue
382
+ elif token[0].isupper():
383
+ esc = html.escape(token)
384
+ element = f'&nbsp;<span style="color: purple; font-weight: bold;">{esc}</span>'
385
+ elif token[0].isdigit():
386
+ esc = html.escape(token)
387
+ element = f'&nbsp;<span style="color: green;">{esc}</span>'
388
+ elif token[0] == '`':
389
+ esc = html.escape(token)
390
+ element = f'&nbsp;<span style="color: peru;">{esc}</span>'
391
+ else:
392
+ esc = html.escape(token)
393
+ element = f'&nbsp;<span>{esc}</span>'
394
+ output += element
395
+ # Colour comment if present
396
+ if comment_part is not None:
397
+ esc = html.escape(comment_part)
398
+ output += f'<span style="color: green;">&nbsp;{esc}</span>'
399
+
400
+ return output
401
+
402
+ ###########################################################################
403
+ # Here when the user clicks a line number
404
+ def onClickLino(self, lino):
405
+ lineSpec = self.scriptLines[lino]
406
+ lineSpec.bp = not lineSpec.bp
407
+ if lineSpec.bp: lineSpec.label.showBlob()
408
+ else: lineSpec.label.hideBlob()
409
+ # Set a breakpoint on this command
410
+ command = self.program.code[self.program.pc]
411
+ command['bp'] = True
412
+ self.program.code[self.program.pc] = command
413
+
414
+ ###########################################################################
415
+ # Scroll to a given line number
416
+ def scrollTo(self, lino):
417
+ # Ensure the line number is valid
418
+ if lino < 0 or lino >= len(self.scriptLines):
419
+ return
420
+
421
+ # Get the panel widget for this line
422
+ lineSpec = self.scriptLines[lino]
423
+ panel = lineSpec.panel
424
+
425
+ if not panel:
426
+ return
427
+
428
+ # Get the scroll area from the right column
429
+ scroll_area = self.rightColumn.scroll
430
+
431
+ # Get the vertical position of the panel relative to the content widget
432
+ panel_y = panel.y()
433
+ panel_height = panel.height()
434
+
435
+ # Get the viewport height (visible area)
436
+ viewport_height = scroll_area.viewport().height()
437
+
438
+ # Calculate the target scroll position to center the panel
439
+ # We want the panel's center to align with the viewport's center
440
+ target_scroll = panel_y + (panel_height // 2) - (viewport_height // 2)
441
+
442
+ # Clamp to valid scroll range
443
+ scrollbar = scroll_area.verticalScrollBar()
444
+ target_scroll = max(scrollbar.minimum(), min(target_scroll, scrollbar.maximum()))
445
+
446
+ # Smoothly scroll to the target position
447
+ scrollbar.setValue(target_scroll)
448
+
449
+ # Bring the window to the front
450
+ self.raise_()
451
+ self.activateWindow()
452
+
453
+ ###########################################################################
454
+ # Here when each instruction is about to run
455
+ def step(self):
456
+ if self.stopped:
457
+ lino=self.program.code[self.program.pc]['lino']
458
+ print(lino)
459
+ self.scrollTo(lino)
460
+ return False
461
+ else:
462
+ if self.program.code[self.program.pc]['bp']:
463
+ pass
464
+ return True
easycoder/ec_handler.py CHANGED
@@ -22,7 +22,7 @@ class Handler:
22
22
  self.compileVariable = compiler.compileVariable
23
23
  self.rewindTo = compiler.rewindTo
24
24
  self.warning = compiler.warning
25
- self.getPC = compiler.getPC
25
+ self.getCodeSize = compiler.getCodeSize
26
26
  self.add = compiler.addCommand
27
27
  self.getCommandAt = compiler.getCommandAt
28
28
  self.compileOne = compiler.compileOne
easycoder/ec_program.py CHANGED
@@ -47,6 +47,9 @@ class Program:
47
47
  self.useClass(Core)
48
48
  self.externalControl = False
49
49
  self.ticker = 0
50
+ self.usingGraphics = False
51
+ self.debugging = False
52
+ self.debugger = None
50
53
  self.running = True
51
54
 
52
55
  # This is called at 10msec intervals by the GUI code
@@ -147,8 +150,9 @@ class Program:
147
150
  name = value['name']
148
151
  symbolRecord = self.getSymbolRecord(name)
149
152
  # if symbolRecord['hasValue']:
150
- handler = self.domainIndex[symbolRecord['domain']].valueHandler('symbol')
151
- result = handler(symbolRecord)
153
+ if symbolRecord:
154
+ handler = self.domainIndex[symbolRecord['domain']].valueHandler('symbol')
155
+ result = handler(symbolRecord)
152
156
  # else:
153
157
  # # Call the given domain to handle a value
154
158
  # # domain = self.domainIndex[value['domain']]
@@ -181,7 +185,10 @@ class Program:
181
185
  return None
182
186
 
183
187
  def getValue(self, value):
184
- return self.evaluate(value).content
188
+ result = self.evaluate(value)
189
+ if result:
190
+ return result.get('content') # type: ignore[union-attr]
191
+ return None
185
192
 
186
193
  def getRuntimeValue(self, value):
187
194
  if value is None:
@@ -287,9 +294,9 @@ class Program:
287
294
  return
288
295
 
289
296
  def releaseParent(self):
290
- if self.parent.waiting and self.parent.program.running:
291
- self.parent.waiting = False
292
- self.parent.program.run(self.parent.pc)
297
+ if self.parent and self.parent.waiting and self.parent.program.running: # type: ignore[union-attr]
298
+ self.parent.waiting = False # type: ignore[union-attr]
299
+ self.parent.program.run(self.parent.pc) # type: ignore[union-attr]
293
300
 
294
301
  # Flush the queue
295
302
  def flush(self, pc):
@@ -306,6 +313,8 @@ class Program:
306
313
  lino = command['lino'] + 1
307
314
  line = self.script.lines[command['lino']].strip()
308
315
  print(f'{self.name}: Line {lino}: {domainName}:{keyword}: {line}')
316
+ if self.debugger != None:
317
+ self.debugger.step()
309
318
  domain = self.domainIndex[domainName]
310
319
  handler = domain.runHandler(keyword)
311
320
  if handler:
@@ -378,9 +387,9 @@ class Program:
378
387
  if type(v2) == int:
379
388
  if type(v1) != int:
380
389
  v2 = f'{v2}'
381
- if v1 > v2:
390
+ if v1 > v2: # type: ignore[operator]
382
391
  return 1
383
- if v1 < v2:
392
+ if v1 < v2: # type: ignore[operator]
384
393
  return -1
385
394
  return 0
386
395
 
easycoder/ec_pyside.py CHANGED
@@ -3,6 +3,7 @@ from functools import partial
3
3
  from .ec_handler import Handler
4
4
  from .ec_classes import RuntimeError, Object
5
5
  from .ec_border import Border
6
+ from .ec_debug import Debugger
6
7
  from PySide6.QtCore import Qt, QTimer, Signal, QRect
7
8
  from PySide6.QtGui import QPixmap, QPainter
8
9
  from PySide6.QtWidgets import (
@@ -835,12 +836,7 @@ class Graphics(Handler):
835
836
  return self.nextPC()
836
837
 
837
838
  # Initialize the graphics environment
838
- def k_init(self, command):
839
- # return True
840
- if self.nextIs('graphics'):
841
- self.add(command)
842
- return True
843
- return False
839
+ # Unused: def k_init(self, command):
844
840
 
845
841
  def r_init(self, command):
846
842
  self.app = QApplication(sys.argv)
@@ -862,6 +858,7 @@ class Graphics(Handler):
862
858
  timer.timeout.connect(flush)
863
859
  timer.start(10)
864
860
  QTimer.singleShot(500, init)
861
+ if self.program.debugging: self.program.debugger = Debugger(self.program)
865
862
  self.app.lastWindowClosed.connect(on_last_window_closed)
866
863
  self.app.exec()
867
864
 
@@ -912,11 +909,11 @@ class Graphics(Handler):
912
909
  # on tick
913
910
  def k_on(self, command):
914
911
  def setupOn():
915
- command['goto'] = self.getPC() + 2
912
+ command['goto'] = self.getCodeSize() + 2
916
913
  self.add(command)
917
914
  self.nextToken()
918
915
  # Step over the click handler
919
- pcNext = self.getPC()
916
+ pcNext = self.getCodeSize()
920
917
  cmd = {}
921
918
  cmd['domain'] = 'core'
922
919
  cmd['lino'] = command['lino']
@@ -933,7 +930,7 @@ class Graphics(Handler):
933
930
  cmd['debug'] = False
934
931
  self.add(cmd)
935
932
  # Fixup the goto
936
- self.getCommandAt(pcNext)['goto'] = self.getPC()
933
+ self.getCommandAt(pcNext)['goto'] = self.getCodeSize()
937
934
 
938
935
  token = self.nextToken()
939
936
  command['type'] = token
@@ -953,11 +950,11 @@ class Graphics(Handler):
953
950
  return True
954
951
  elif token == 'tick':
955
952
  command['tick'] = True
956
- command['runOnTick'] = self.getPC() + 2
953
+ command['runOnTick'] = self.getCodeSize() + 2
957
954
  self.add(command)
958
955
  self.nextToken()
959
956
  # Step over the on tick action
960
- pcNext = self.getPC()
957
+ pcNext = self.getCodeSize()
961
958
  cmd = {}
962
959
  cmd['domain'] = 'core'
963
960
  cmd['lino'] = command['lino']
@@ -974,7 +971,7 @@ class Graphics(Handler):
974
971
  cmd['debug'] = False
975
972
  self.add(cmd)
976
973
  # Fixup the goto
977
- self.getCommandAt(pcNext)['goto'] = self.getPC()
974
+ self.getCommandAt(pcNext)['goto'] = self.getCodeSize()
978
975
  return True
979
976
  return False
980
977
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easycoder
3
- Version: 251103.2
3
+ Version: 251104.2
4
4
  Summary: Rapid scripting in English
5
5
  Keywords: compiler,scripting,prototyping,programming,coding,python,low code,hypertalk,computer language,learn to code
6
6
  Author-email: Graham Trott <gtanyware@gmail.com>
@@ -0,0 +1,20 @@
1
+ easycoder/__init__.py,sha256=MRZ5qqYf7XUeRfrculBoOmXvlayyAdVqU7wI5n47Bqs,339
2
+ easycoder/close.png,sha256=3B9ueRNtEu9E4QNmZhdyC4VL6uqKvGmdfeFxIV9aO_Y,9847
3
+ easycoder/ec_border.py,sha256=KpOy0Jq8jI_6DYGo4jaFvoBP_jTIoAYWrmuHhl-FXA4,2355
4
+ easycoder/ec_classes.py,sha256=EWpB3Wta_jvKZ8SNIWua_ElIbw1FzKMyM3_IiXBn-lg,1995
5
+ easycoder/ec_compiler.py,sha256=Q6a9nMmZogJzHu8OB4VeMzmBBarVEl3-RkqH2gW4LiU,6599
6
+ easycoder/ec_condition.py,sha256=uamZrlW3Ej3R4bPDuduGB2f00M80Z1D0qV8muDx4Qfw,784
7
+ easycoder/ec_core.py,sha256=F8MQ1dJfzQoYxTrCyJ7aUrmyxjQEW4dmWd9uq71rR0w,100876
8
+ easycoder/ec_debug.py,sha256=JrF8GDzymB2YCOuULtlot4jO1yiVEbt_hQqjZPDcPEI,18785
9
+ easycoder/ec_handler.py,sha256=doGCMXBCQxvSaD3omKMlXoR_LvQODxV7dZyoWafecyg,2336
10
+ easycoder/ec_keyboard.py,sha256=H8DhPT8-IvAIGgRxCs4qSaF_AQLaS6lxxtDteejbktM,19010
11
+ easycoder/ec_program.py,sha256=biRWYKUs7ywFEgTte0oerTyPxgZlP4odfv_xJAN7ao8,10696
12
+ easycoder/ec_pyside.py,sha256=WDzJn64fPKMJCwTBXjngCRkJyS4fA72FmwfUoqul-Hw,58400
13
+ easycoder/ec_timestamp.py,sha256=myQnnF-mT31_1dpQKv2VEAu4BCcbypvMdzq7_DUi1xc,277
14
+ easycoder/ec_value.py,sha256=zgDJTJhIg3yOvmnnKIfccIizmIhGbtvL_ghLTL1T5fg,2516
15
+ easycoder/tick.png,sha256=OedASXJJTYvnza4J6Kv5m5lz6DrBfy667zX_WGgtbmM,9127
16
+ easycoder-251104.2.dist-info/entry_points.txt,sha256=JXAZbenl0TnsIft2FcGJbJ-4qoztVu2FuT8PFmWFexM,44
17
+ easycoder-251104.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
+ easycoder-251104.2.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
19
+ easycoder-251104.2.dist-info/METADATA,sha256=SFh8m9keUUTbvuC0q4R3n7lCuBA_whA4Q2Pt8s1EMWc,6897
20
+ easycoder-251104.2.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- easycoder/__init__.py,sha256=Lcn-X3_bWT1Leeh_wHMEaDuMq_A2Zm4GWMxwo3Ko7qI,339
2
- easycoder/close.png,sha256=3B9ueRNtEu9E4QNmZhdyC4VL6uqKvGmdfeFxIV9aO_Y,9847
3
- easycoder/ec_border.py,sha256=KpOy0Jq8jI_6DYGo4jaFvoBP_jTIoAYWrmuHhl-FXA4,2355
4
- easycoder/ec_classes.py,sha256=YGUiKnVN6T5scoeBmmGDQAtE8xJgaTHi0Exh9A7H2Y4,1750
5
- easycoder/ec_compiler.py,sha256=ZtI40G7GTkCeOYMx66TmU_SwauoktAPW3njhFWESK6k,5505
6
- easycoder/ec_condition.py,sha256=uamZrlW3Ej3R4bPDuduGB2f00M80Z1D0qV8muDx4Qfw,784
7
- easycoder/ec_core.py,sha256=c51wdwTwppZwGLOhN22SBCdAmnTFQw0eirMJxkOb8vI,100780
8
- easycoder/ec_handler.py,sha256=zEZ5cPruEVZp3SIQ6ZjdZN5jDyW2gFvHcNmFaet4Sd4,2324
9
- easycoder/ec_keyboard.py,sha256=H8DhPT8-IvAIGgRxCs4qSaF_AQLaS6lxxtDteejbktM,19010
10
- easycoder/ec_program.py,sha256=aPuZOYWFqGd1jsLDl5M5YmLu5LfFAewF9EZh6zHIbyM,10308
11
- easycoder/ec_pyside.py,sha256=Pkf0lgDTgC7WOjjleiGeBj4e9y9de4DDNlZFsajkVjU,58374
12
- easycoder/ec_timestamp.py,sha256=myQnnF-mT31_1dpQKv2VEAu4BCcbypvMdzq7_DUi1xc,277
13
- easycoder/ec_value.py,sha256=zgDJTJhIg3yOvmnnKIfccIizmIhGbtvL_ghLTL1T5fg,2516
14
- easycoder/tick.png,sha256=OedASXJJTYvnza4J6Kv5m5lz6DrBfy667zX_WGgtbmM,9127
15
- easycoder-251103.2.dist-info/entry_points.txt,sha256=JXAZbenl0TnsIft2FcGJbJ-4qoztVu2FuT8PFmWFexM,44
16
- easycoder-251103.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
17
- easycoder-251103.2.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
18
- easycoder-251103.2.dist-info/METADATA,sha256=OntVjUUCq5GZxKP5eSqo8iKBG4WTL0tQ5ocEge4Pzbc,6897
19
- easycoder-251103.2.dist-info/RECORD,,