import math import sys from PySide6.QtCore import Qt, QRect, QSize from PySide6.QtGui import ( QColor, QFont, QPainter, QSyntaxHighlighter, QTextCharFormat, ) from PySide6.QtWidgets import ( QApplication, QDockWidget, QFrame, QHBoxLayout, QLabel, QLineEdit, QListWidget, QMainWindow, QPlainTextEdit, QPushButton, QSplitter, QStatusBar, QTabWidget, QTableWidget, QTableWidgetItem, QToolBar, QVBoxLayout, QWidget, ) from abacus_core import AbacusCore STYLE = """ QMainWindow, QWidget { background: #0d1117; color: #e6edf3; font-family: "Segoe UI"; font-size: 13px; } QToolBar { background: #111820; border: 0; border-bottom: 1px solid #202a36; spacing: 5px; padding: 6px 8px; } QToolButton { color: #c9d1d9; background: transparent; border-radius: 7px; padding: 7px 10px; } QToolButton:hover { background: #1b2531; color: #ffffff; } QTabWidget::pane { border: 1px solid #202a36; background: #0d1117; } QTabBar::tab { background: #111820; color: #7d8b99; border: 1px solid #202a36; border-bottom: none; padding: 9px 16px; margin-right: 2px; } QTabBar::tab:selected { background: #17212c; color: #f0f6fc; } QDockWidget { color: #aeb9c6; titlebar-close-icon: none; titlebar-normal-icon: none; } QDockWidget::title { background: #111820; border-bottom: 1px solid #202a36; padding: 9px 11px; font-weight: 700; } QFrame#editorCard, QFrame#bottomCard { background: #111820; border: 1px solid #202a36; border-radius: 10px; } QPlainTextEdit { background: #0b0f14; color: #d6deea; border: 1px solid #202a36; border-radius: 9px; selection-background-color: #244a7d; padding: 8px; font-family: "JetBrains Mono", "Consolas", monospace; font-size: 14px; } QLineEdit#singleLine { background: #0b0f14; color: #f0f6fc; border: 1px solid #303b48; border-radius: 9px; padding: 9px 11px; font-family: "JetBrains Mono", "Consolas", monospace; } QLineEdit#singleLine:focus { border: 1px solid #3b82f6; } QPushButton { background: #2563eb; color: white; border: none; border-radius: 8px; padding: 8px 13px; font-weight: 700; } QPushButton:hover { background: #3475ef; } QPushButton#operator { background: #1b2531; color: #cbd5e1; border: 1px solid #293543; min-width: 34px; } QPushButton#operator:hover { background: #253344; color: #ffffff; } QPushButton#secondary { background: #1b2531; color: #b9c4d0; border: 1px solid #293543; } QListWidget, QTableWidget { background: #0d131a; color: #cbd5e1; border: 1px solid #202a36; border-radius: 8px; outline: none; } QListWidget::item { padding: 7px 8px; border-radius: 6px; } QListWidget::item:hover { background: #17212c; } QListWidget::item:selected { background: #1c3153; color: #ffffff; } QHeaderView::section { background: #161f29; color: #9aa8b7; border: none; border-right: 1px solid #202a36; border-bottom: 1px solid #202a36; padding: 7px; } QTableWidget::item { padding: 6px; border-bottom: 1px solid #18212b; } QStatusBar { background: #0b0f14; color: #7d8b99; border-top: 1px solid #202a36; } QScrollBar:vertical { background: #0d131a; width: 9px; } QScrollBar::handle:vertical { background: #303c4a; border-radius: 4px; min-height: 25px; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; } """ class AbacusHighlighter(QSyntaxHighlighter): def __init__(self, document): super().__init__(document) self.number = QTextCharFormat() self.number.setForeground(QColor("#79c0ff")) self.operator = QTextCharFormat() self.operator.setForeground(QColor("#ff7b72")) self.comment = QTextCharFormat() self.comment.setForeground(QColor("#6e7681")) self.comment.setFontItalic(True) self.assignment = QTextCharFormat() self.assignment.setForeground(QColor("#d2a8ff")) self.keyword = QTextCharFormat() self.keyword.setForeground(QColor("#ffa657")) def highlightBlock(self, text): # Comments comment_pos = text.find("#") if comment_pos >= 0: self.setFormat(comment_pos, len(text) - comment_pos, self.comment) code = text[:comment_pos] else: code = text # Numbers i = 0 while i < len(code): if code[i].isdigit() or ( code[i] == "." and i + 1 < len(code) and code[i + 1].isdigit() ): start = i i += 1 while i < len(code) and (code[i].isdigit() or code[i] == "."): i += 1 self.setFormat(start, i - start, self.number) else: i += 1 for char in "+-*/=():[];": start = 0 while True: pos = code.find(char, start) if pos < 0: break self.setFormat(pos, 1, self.operator) start = pos + 1 # Function definitions: name before ':' colon = code.find(":") if colon > 0: name = code[:colon].strip() if name: pos = code.find(name) self.setFormat(pos, len(name), self.keyword) class LineNumberArea(QWidget): def __init__(self, editor): super().__init__(editor) self.editor = editor def sizeHint(self): return QSize(self.editor.line_number_width(), 0) def paintEvent(self, event): painter = QPainter(self) painter.fillRect(event.rect(), QColor("#0f151c")) block = self.editor.firstVisibleBlock() block_number = block.blockNumber() top = int( self.editor.blockBoundingGeometry(block).translated( self.editor.contentOffset() ).top() ) bottom = top + int(self.editor.blockBoundingRect(block).height()) while block.isValid() and top <= event.rect().bottom(): if block.isVisible() and bottom >= event.rect().top(): painter.setPen(QColor("#596878")) painter.drawText( 0, top, self.width() - 7, self.editor.fontMetrics().height(), Qt.AlignRight, str(block_number + 1), ) block = block.next() top = bottom bottom = top + int(self.editor.blockBoundingRect(block).height()) block_number += 1 class CodeEditor(QPlainTextEdit): def __init__(self): super().__init__() self.line_numbers = LineNumberArea(self) self.blockCountChanged.connect(self.update_line_number_width) self.updateRequest.connect(self.update_line_number_area) self.cursorPositionChanged.connect(self.highlight_current_line) self.update_line_number_width(0) self.highlight_current_line() font = QFont("JetBrains Mono", 11) font.setStyleHint(QFont.Monospace) self.setFont(font) self.setTabStopDistance( self.fontMetrics().horizontalAdvance(" ") * 4 ) def line_number_width(self): digits = len(str(max(1, self.blockCount()))) return 12 + self.fontMetrics().horizontalAdvance("9") * digits def update_line_number_width(self, _): self.setViewportMargins(self.line_number_width(), 0, 0, 0) def update_line_number_area(self, rect, dy): if dy: self.line_numbers.scroll(0, dy) else: self.line_numbers.update(0, rect.y(), self.line_numbers.width(), rect.height()) if rect.contains(self.viewport().rect()): self.update_line_number_width(0) def resizeEvent(self, event): super().resizeEvent(event) rect = self.contentsRect() rect.setWidth(self.line_number_width()) self.line_numbers.setGeometry(rect) def highlight_current_line(self): extra = [] if not self.isReadOnly(): selection = QTextCharFormat() selection.setBackground(QColor("#111a24")) selection.setProperty(QTextCharFormat.FullWidthSelection, True) extra.append((self.textCursor(), selection)) self.setExtraSelections(extra) class ResultGraph(QWidget): """Small dependency-free graph of numeric results.""" def __init__(self): super().__init__() self.values = [] def set_values(self, values): self.values = values[-50:] self.update() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) painter.fillRect(self.rect(), QColor("#0b0f14")) r = self.rect().adjusted(45, 20, -20, -35) painter.setPen(QColor("#273442")) painter.drawRect(r) if not self.values: painter.setPen(QColor("#647383")) painter.drawText(r, Qt.AlignCenter, "No numeric results yet") return numeric = [v for v in self.values if math.isfinite(v)] if not numeric: return lo = min(numeric) hi = max(numeric) if abs(hi - lo) < 1e-12: lo -= 1 hi += 1 painter.setPen(QColor("#344454")) for frac in (0.0, 0.5, 1.0): y = int(r.bottom() - frac * r.height()) painter.drawLine(r.left(), y, r.right(), y) painter.setPen(QColor("#5f6f80")) painter.drawText(5, r.top() + 5, 35, 20, Qt.AlignRight, f"{hi:g}") painter.drawText(5, r.bottom() - 15, 35, 20, Qt.AlignRight, f"{lo:g}") if len(numeric) == 1: x = r.center().x() y = r.bottom() - (numeric[0] - lo) / (hi - lo) * r.height() painter.setBrush(QColor("#58a6ff")) painter.drawEllipse(int(x - 4), int(y - 4), 8, 8) return points = [] count = len(numeric) for i, value in enumerate(numeric): x = r.left() + i * r.width() / max(1, count - 1) y = r.bottom() - (value - lo) / (hi - lo) * r.height() points.append((x, y)) painter.setPen(QColor("#58a6ff")) for a, b in zip(points, points[1:]): painter.drawLine(int(a[0]), int(a[1]), int(b[0]), int(b[1])) painter.setBrush(QColor("#58a6ff")) for x, y in points: painter.drawEllipse(int(x - 3), int(y - 3), 6, 6) class AbacusIDE(QMainWindow): def __init__(self): super().__init__() self.core = AbacusCore() self.step = 0 self.results = [] self.setWindowTitle("Abacus IDE") self.resize(1400, 850) self.setMinimumSize(1050, 650) self._build_toolbar() self._build_main() self._build_docks() self._build_statusbar() self._refresh_sidebars() self.editor.setFocus() def _build_toolbar(self): toolbar = QToolBar() toolbar.setMovable(False) toolbar.setToolButtonStyle(Qt.ToolButtonTextOnly) self.addToolBar(toolbar) run = toolbar.addAction("▶ Run") run.triggered.connect(self.run_document) run_line = toolbar.addAction("Run line") run_line.triggered.connect(self.run_current_line) toolbar.addSeparator() for symbol in ("+", "-", "*", "/", "(", ")", "[", "]", "="): button = QPushButton(symbol) button.setObjectName("operator") button.clicked.connect( lambda checked=False, s=symbol: self.insert_operator(s) ) toolbar.addWidget(button) toolbar.addSeparator() clear = toolbar.addAction("Clear") clear.triggered.connect(self.editor.clear) toolbar.addSeparator() reset = toolbar.addAction("Reset") reset.triggered.connect(self.reset_abacus) def _build_main(self): self.tabs = QTabWidget() self.setCentralWidget(self.tabs) # Workspace tab workspace = QWidget() workspace_layout = QVBoxLayout(workspace) workspace_layout.setContentsMargins(10, 10, 10, 10) workspace_layout.setSpacing(8) editor_header = QHBoxLayout() label = QLabel("Expression editor") label.setStyleSheet( "font-size: 13px; font-weight: 700; color: #c9d1d9;" ) hint = QLabel( "One command per line • # comments • Enter = run line" ) hint.setStyleSheet("color: #657585;") editor_header.addWidget(label) editor_header.addStretch() editor_header.addWidget(hint) workspace_layout.addLayout(editor_header) self.editor = CodeEditor() AbacusHighlighter(self.editor.document()) self.editor.setPlainText( "# Example\n" "alpha = 10\n" "beta = 2\n" "alpha + beta * 3\n" ) workspace_layout.addWidget(self.editor, 1) bottom = QFrame() bottom.setObjectName("bottomCard") bottom_layout = QHBoxLayout(bottom) bottom_layout.setContentsMargins(10, 8, 10, 8) bottom_layout.addWidget(QLabel("Quick input:")) self.single_line = QLineEdit() self.single_line.setObjectName("singleLine") self.single_line.setPlaceholderText("Enter expression...") self.single_line.returnPressed.connect(self.run_single_line) bottom_layout.addWidget(self.single_line, 1) execute = QPushButton("Execute") execute.clicked.connect(self.run_single_line) bottom_layout.addWidget(execute) workspace_layout.addWidget(bottom) self.tabs.addTab(workspace, "Workspace") # Results tab results_page = QWidget() results_layout = QVBoxLayout(results_page) results_layout.setContentsMargins(10, 10, 10, 10) self.results_table = QTableWidget(0, 4) self.results_table.setHorizontalHeaderLabels( ["Step", "Expression", "Result", "Comment"] ) self.results_table.horizontalHeader().setStretchLastSection(True) self.results_table.setAlternatingRowColors(False) results_layout.addWidget(self.results_table) self.tabs.addTab(results_page, "Results") # Graph tab graph_page = QWidget() graph_layout = QVBoxLayout(graph_page) graph_layout.setContentsMargins(10, 10, 10, 10) graph_title = QLabel("Numeric result history") graph_title.setStyleSheet( "font-size: 13px; font-weight: 700; color: #c9d1d9;" ) graph_layout.addWidget(graph_title) self.graph = ResultGraph() graph_layout.addWidget(self.graph, 1) graph_info = QLabel( "The graph uses numeric results from executed expressions." ) graph_info.setStyleSheet("color: #657585;") graph_layout.addWidget(graph_info) self.tabs.addTab(graph_page, "Graph") def _build_docks(self): self.variables_dock = self._make_dock( "Variables", self._make_list_widget() ) self.functions_dock = self._make_dock( "Functions", self._make_list_widget() ) self.history_dock = self._make_dock( "History", self._make_list_widget() ) self.variables_list = self.variables_dock.widget() self.functions_list = self.functions_dock.widget() self.history_list = self.history_dock.widget() self.addDockWidget(Qt.RightDockWidgetArea, self.variables_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.functions_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.history_dock) self.tabifyDockWidget(self.variables_dock, self.functions_dock) self.tabifyDockWidget(self.functions_dock, self.history_dock) self.variables_dock.raise_() def _make_list_widget(self): return QListWidget() def _make_dock(self, title, widget): dock = QDockWidget(title, self) dock.setAllowedAreas( Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea ) dock.setFeatures( QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable ) dock.setWidget(widget) return dock def _build_statusbar(self): self.status = QStatusBar() self.setStatusBar(self.status) self.status.showMessage("Ready") def insert_operator(self, symbol): self.editor.insertPlainText(symbol) self.editor.setFocus() def run_single_line(self): text = self.single_line.text().strip() if not text: return self._execute(text) self.single_line.clear() def run_current_line(self): cursor = self.editor.textCursor() cursor.select(cursor.LineUnderCursor) line = cursor.selectedText().strip() if line: self._execute(line) def run_document(self): lines = self.editor.toPlainText().splitlines() executed = 0 for line in lines: line = line.strip() if not line or line.startswith("#"): continue self._execute(line) executed += 1 self.status.showMessage(f"Executed {executed} command(s)") def _execute(self, text): self.step += 1 try: result = self.core.parse_input(text) except Exception as exc: self.step -= 1 self.status.showMessage(f"Error: {exc}") self.history_list.addItem(f"ERROR {text} → {exc}") return # Critical: assignments/functions return None, but their # dictionaries are already changed. Refresh immediately. self._refresh_sidebars() if not result: self.status.showMessage(f"Updated: {text}") return original, _, value, comment = result value_text = str(value) comment_text = comment or "" self.results.append( { "step": self.step, "expression": original, "result": value_text, "comment": comment_text, } ) self.history_list.addItem( f"{self.step}. {original} = {value_text}" ) self.history_list.scrollToBottom() self._add_result_row( self.step, original, value_text, comment_text ) try: numeric = float(value) except (TypeError, ValueError): numeric = None if numeric is not None and math.isfinite(numeric): self.graph.values.append(numeric) self.graph.set_values(self.graph.values) self.status.showMessage(f"Result: {value_text}") def _add_result_row(self, step, expression, result, comment): row = self.results_table.rowCount() self.results_table.insertRow(row) values = [str(step), expression, result, comment] for col, value in enumerate(values): self.results_table.setItem( row, col, QTableWidgetItem(value) ) self.results_table.resizeColumnsToContents() self.results_table.scrollToBottom() def _refresh_sidebars(self): self.variables_list.clear() for key, value in self.core.get_vars().items(): self.variables_list.addItem(f"{key} = {value}") self.functions_list.clear() for key, value in self.core.get_funcs().items(): self.functions_list.addItem(f"{key} = {value}") def reset_abacus(self): self.core = AbacusCore() self.step = 0 self.results.clear() self.variables_list.clear() self.functions_list.clear() self.history_list.clear() self.results_table.setRowCount(0) self.graph.set_values([]) self.status.showMessage("Abacus state reset") def main(): app = QApplication(sys.argv) app.setStyle("Fusion") app.setStyleSheet(STYLE) window = AbacusIDE() window.show() sys.exit(app.exec()) if __name__ == "__main__": main()