From 3f18b0faae381f901ee2cd2eb99defa51bf82cc8 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 11 Aug 2026 23:11:24 +0200 Subject: [PATCH] worked on gui pyside6 --- .gitignore | 2 + abacus_ide.py | 761 +++++++++++++++++++++ abacus_ide_checked.py | 774 ++++++++++++++++++++++ abacus_ide_checked_final.py | 1091 +++++++++++++++++++++++++++++++ abacus_ide_fixed.py | 761 +++++++++++++++++++++ abacus_ide_fixed_v2.py | 764 ++++++++++++++++++++++ abacus_ide_json_autocomplete.py | 1057 ++++++++++++++++++++++++++++++ abacus_pyside6.py | 354 ++++++++++ abacus_pyside6_v2.py | 360 ++++++++++ dieselfloater_edeka.json | 30 + test.json | 46 ++ 11 files changed, 6000 insertions(+) create mode 100644 .gitignore create mode 100644 abacus_ide.py create mode 100644 abacus_ide_checked.py create mode 100644 abacus_ide_checked_final.py create mode 100644 abacus_ide_fixed.py create mode 100644 abacus_ide_fixed_v2.py create mode 100644 abacus_ide_json_autocomplete.py create mode 100644 abacus_pyside6.py create mode 100644 abacus_pyside6_v2.py create mode 100644 dieselfloater_edeka.json create mode 100644 test.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb53ea0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.venv +/.idea \ No newline at end of file diff --git a/abacus_ide.py b/abacus_ide.py new file mode 100644 index 0000000..91ef67b --- /dev/null +++ b/abacus_ide.py @@ -0,0 +1,761 @@ +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() diff --git a/abacus_ide_checked.py b/abacus_ide_checked.py new file mode 100644 index 0000000..08be22e --- /dev/null +++ b/abacus_ide_checked.py @@ -0,0 +1,774 @@ +import math +import sys + +from PySide6.QtCore import Qt, QSize +from PySide6.QtGui import ( + QColor, + QFont, + QPainter, + QSyntaxHighlighter, + QTextCharFormat, +) +from PySide6.QtWidgets import ( + QApplication, + QDockWidget, + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QMainWindow, + QPlainTextEdit, + QTextEdit, + QPushButton, + 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_selection = QTextEdit.ExtraSelection() + extra_selection.cursor = self.textCursor() + extra_selection.format = selection + extra.append(extra_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_main() + self._build_toolbar() + 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() + self.highlighter = 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 + failed = 0 + + for line in lines: + line = line.strip() + if not line or line.startswith("#"): + continue + + if self._execute(line): + executed += 1 + else: + failed += 1 + + if failed: + self.status.showMessage( + f"Executed {executed} command(s), {failed} error(s)" + ) + else: + 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 False + + # 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 True + + 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}") + return True + + 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() diff --git a/abacus_ide_checked_final.py b/abacus_ide_checked_final.py new file mode 100644 index 0000000..5427fcf --- /dev/null +++ b/abacus_ide_checked_final.py @@ -0,0 +1,1091 @@ +import json +import math +import sys + +from PySide6.QtCore import Qt, QSize, QStringListModel +from PySide6.QtGui import ( + QColor, + QFont, + QPainter, + QSyntaxHighlighter, + QTextCharFormat, + QTextCursor, +) +from PySide6.QtWidgets import ( + QApplication, + QDockWidget, + QFrame, + QCompleter, + QFileDialog, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QMainWindow, + QPlainTextEdit, + QTextEdit, + QPushButton, + 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.error_lines = {} + self.completer = None + + 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_selection = QTextEdit.ExtraSelection() + extra_selection.cursor = self.textCursor() + extra_selection.format = selection + extra.append(extra_selection) + + # Error lines always get a stronger red background. + for line_number, message in self.error_lines.items(): + block = self.document().findBlockByNumber(line_number - 1) + if not block.isValid(): + continue + + cursor = self.textCursor() + cursor.setPosition(block.position()) + cursor.setPosition( + block.position() + max(1, block.length() - 1), + QTextCursor.KeepAnchor, + ) + + selection = QTextCharFormat() + selection.setBackground(QColor("#4a1f25")) + selection.setForeground(QColor("#ffb4b4")) + selection.setProperty( + QTextCharFormat.FullWidthSelection, + True, + ) + + extra_selection = QTextEdit.ExtraSelection() + extra_selection.cursor = cursor + extra_selection.format = selection + extra.append(extra_selection) + + self.setExtraSelections(extra) + + self.line_numbers.update() + + def set_error_lines(self, errors): + self.error_lines = dict(errors) + self.highlight_current_line() + + def clear_error_lines(self): + self.error_lines.clear() + self.highlight_current_line() + + def set_completer(self, completer): + self.completer = completer + completer.setWidget(self) + completer.setCompletionMode(QCompleter.PopupCompletion) + completer.setCaseSensitivity(Qt.CaseInsensitive) + completer.activated.connect(self.insert_completion) + + def insert_completion(self, completion): + prefix = self.completion_prefix() + cursor = self.textCursor() + + if prefix: + cursor.movePosition( + QTextCursor.Left, + QTextCursor.KeepAnchor, + len(prefix), + ) + cursor.insertText(completion) + else: + cursor.insertText(completion) + + self.setTextCursor(cursor) + + def completion_prefix(self): + cursor = self.textCursor() + text = cursor.block().text() + pos = cursor.positionInBlock() + + start = pos + while start > 0: + char = text[start - 1] + if not (char.isalnum() or char == "_"): + break + start -= 1 + + return text[start:pos] + + def show_completion(self, show_all=False): + if not self.completer: + return + + prefix = self.completion_prefix() + if not prefix and not show_all: + self.completer.popup().hide() + return + + self.completer.setCompletionPrefix(prefix) + + if show_all: + self.completer.setCompletionPrefix("") + + if self.completer.completionCount() == 0: + self.completer.popup().hide() + return + + rect = self.cursorRect() + rect.setWidth( + self.completer.popup().sizeHintForColumn(0) + + self.completer.popup().verticalScrollBar().sizeHint().width() + ) + self.completer.complete(rect) + + def keyPressEvent(self, event): + if self.completer and self.completer.popup().isVisible(): + if event.key() in ( + Qt.Key_Enter, + Qt.Key_Return, + Qt.Key_Tab, + Qt.Key_Backtab, + ): + event.ignore() + return + + if event.key() == Qt.Key_Escape: + self.completer.popup().hide() + return + + if ( + event.key() == Qt.Key_Space + and event.modifiers() & Qt.ControlModifier + ): + self.show_completion(show_all=True) + return + + super().keyPressEvent(event) + + if self.completer: + self.show_completion() + +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_main() + self._build_menu() + self._build_toolbar() + self._build_docks() + self._build_statusbar() + + self._refresh_sidebars() + self.editor.setFocus() + + def _build_menu(self): + menu = self.menuBar() + + file_menu = menu.addMenu("File") + + export_action = file_menu.addAction("Export history to JSON...") + export_action.triggered.connect(self.export_history) + + import_action = file_menu.addAction("Import history from JSON...") + import_action.triggered.connect(self.import_history) + + file_menu.addSeparator() + + reset_action = file_menu.addAction("Reset Abacus") + reset_action.triggered.connect(self.reset_abacus) + + file_menu.addSeparator() + + exit_action = file_menu.addAction("Exit") + exit_action.triggered.connect(self.close) + + edit_menu = menu.addMenu("Edit") + clear_errors = edit_menu.addAction("Clear error highlighting") + clear_errors.triggered.connect(self.editor.clear_error_lines) + + 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() + self.highlighter = AbacusHighlighter(self.editor.document()) + + self.completion_model = QStringListModel() + self.completer = QCompleter(self.completion_model, self) + self.editor.set_completer(self.completer) + + 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.editor.clear_error_lines() + line_number = cursor.blockNumber() + 1 + self._execute(line, line_number) + + def run_document(self): + self.editor.clear_error_lines() + lines = self.editor.toPlainText().splitlines() + + executed = 0 + failed = 0 + + for line_index, line in enumerate(lines, start=1): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + + if self._execute(stripped, line_index): + executed += 1 + else: + failed += 1 + + if failed: + self.status.showMessage( + f"Executed {executed} command(s), {failed} error(s)" + ) + else: + self.status.showMessage(f"Executed {executed} command(s)") + + def _execute(self, text, line_number=None): + self.step += 1 + + try: + result = self.core.parse_input(text) + except Exception as exc: + self.step -= 1 + self.status.showMessage(f"Error: {exc}") + + if line_number is not None: + self.editor.set_error_lines({ + line_number: str(exc), + }) + + self.history_list.addItem( + f"ERROR line {line_number or '?'} {text} → {exc}" + ) + return False + + # 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 True + + 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}") + return True + + 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() + + functions = self.core.get_funcs() + + for key, value in functions.items(): + self.functions_list.addItem(f"{key} = {value}") + + names = list(self.core.get_vars().keys()) + list(functions.keys()) + self.completion_model.setStringList(sorted(set(names))) + + def export_history(self): + path, _ = QFileDialog.getSaveFileName( + self, + "Export history", + "abacus_history.json", + "JSON files (*.json)", + ) + if not path: + return + + data = { + "format": "abacus-history", + "version": 1, + "step": self.step, + "document": self.editor.toPlainText(), + "variables": self.core.get_vars(), + "functions": self.core.get_funcs(), + "history": [ + self.history_list.item(i).text() + for i in range(self.history_list.count()) + ], + "results": self.results, + "graph_values": self.graph.values, + } + + try: + with open(path, "w", encoding="utf-8") as file: + json.dump( + data, + file, + ensure_ascii=False, + indent=2, + ) + except OSError as exc: + self.status.showMessage(f"Export error: {exc}") + return + + self.status.showMessage(f"History exported: {path}") + + def import_history(self): + path, _ = QFileDialog.getOpenFileName( + self, + "Import history", + "", + "JSON files (*.json)", + ) + if not path: + return + + try: + with open(path, "r", encoding="utf-8") as file: + data = json.load(file) + + if data.get("format") != "abacus-history": + raise ValueError("Not an Abacus history file") + + if data.get("version") != 1: + raise ValueError( + f"Unsupported history version: {data.get('version')}" + ) + + history = data.get("history", []) + results = data.get("results", []) + graph_values = data.get("graph_values", []) + variables = data.get("variables", {}) + functions = data.get("functions", {}) + document = data.get("document", "") + step = int(data.get("step", len(results))) + + if not isinstance(history, list): + raise ValueError("Invalid history data") + if not isinstance(results, list): + raise ValueError("Invalid results data") + if not all(isinstance(item, dict) for item in results): + raise ValueError("Invalid result item") + if not isinstance(graph_values, list): + raise ValueError("Invalid graph data") + if not isinstance(variables, dict): + raise ValueError("Invalid variables data") + if not isinstance(functions, dict): + raise ValueError("Invalid functions data") + if not isinstance(document, str): + raise ValueError("Invalid document data") + + imported_graph_values = [] + for value in graph_values: + number = float(value) + if not math.isfinite(number): + raise ValueError("Graph contains a non-finite value") + imported_graph_values.append(number) + + except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc: + self.status.showMessage(f"Import error: {exc}") + return + + self.step = step + self.results = results + + self.core = AbacusCore() + self.core.get_vars().clear() + self.core.get_vars().update(variables) + self.core.get_funcs().clear() + self.core.get_funcs().update(functions) + + self.history_list.clear() + self.history_list.addItems( + str(item) for item in history + ) + + self.results_table.setRowCount(0) + for item in self.results: + self._add_result_row( + item.get("step", ""), + item.get("expression", ""), + item.get("result", ""), + item.get("comment", ""), + ) + + self.graph.set_values(imported_graph_values) + + self.editor.setPlainText(document) + self.editor.clear_error_lines() + self._refresh_sidebars() + + self.status.showMessage(f"History imported: {path}") + + 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.editor.clear_error_lines() + self._refresh_sidebars() + 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() diff --git a/abacus_ide_fixed.py b/abacus_ide_fixed.py new file mode 100644 index 0000000..a9c089e --- /dev/null +++ b/abacus_ide_fixed.py @@ -0,0 +1,761 @@ +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_main() + self._build_toolbar() + 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() diff --git a/abacus_ide_fixed_v2.py b/abacus_ide_fixed_v2.py new file mode 100644 index 0000000..8a29f68 --- /dev/null +++ b/abacus_ide_fixed_v2.py @@ -0,0 +1,764 @@ +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_selection = self.ExtraSelection() + extra_selection.cursor = self.textCursor() + extra_selection.format = selection + extra.append(extra_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_main() + self._build_toolbar() + 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() diff --git a/abacus_ide_json_autocomplete.py b/abacus_ide_json_autocomplete.py new file mode 100644 index 0000000..9b2311e --- /dev/null +++ b/abacus_ide_json_autocomplete.py @@ -0,0 +1,1057 @@ +import json +import math +import sys + +from PySide6.QtCore import Qt, QSize, QStringListModel +from PySide6.QtGui import ( + QColor, + QFont, + QPainter, + QSyntaxHighlighter, + QTextCharFormat, +) +from PySide6.QtWidgets import ( + QApplication, + QDockWidget, + QFrame, + QCompleter, + QFileDialog, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QMainWindow, + QPlainTextEdit, + QTextEdit, + QTextCursor, + QPushButton, + 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.error_lines = {} + self.completer = None + + 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_selection = QTextEdit.ExtraSelection() + extra_selection.cursor = self.textCursor() + extra_selection.format = selection + extra.append(extra_selection) + + # Error lines always get a stronger red background. + for line_number, message in self.error_lines.items(): + block = self.document().findBlockByNumber(line_number - 1) + if not block.isValid(): + continue + + cursor = self.textCursor() + cursor.setPosition(block.position()) + cursor.setPosition( + block.position() + max(1, block.length() - 1), + QTextCursor.KeepAnchor, + ) + + selection = QTextCharFormat() + selection.setBackground(QColor("#4a1f25")) + selection.setForeground(QColor("#ffb4b4")) + selection.setProperty( + QTextCharFormat.FullWidthSelection, + True, + ) + + extra_selection = QTextEdit.ExtraSelection() + extra_selection.cursor = cursor + extra_selection.format = selection + extra.append(extra_selection) + + self.setExtraSelections(extra) + + self.line_numbers.update() + + def set_error_lines(self, errors): + self.error_lines = dict(errors) + self.highlight_current_line() + + def clear_error_lines(self): + self.error_lines.clear() + self.highlight_current_line() + + def set_completer(self, completer): + self.completer = completer + completer.setWidget(self) + completer.setCompletionMode(QCompleter.PopupCompletion) + completer.setCaseSensitivity(Qt.CaseInsensitive) + completer.activated.connect(self.insert_completion) + + def insert_completion(self, completion): + cursor = self.textCursor() + extra = len(completion) - len(self.completion_prefix()) + if extra > 0: + cursor.insertText(completion[-extra:]) + self.setTextCursor(cursor) + + def completion_prefix(self): + cursor = self.textCursor() + text = cursor.block().text() + pos = cursor.positionInBlock() + + start = pos + while start > 0: + char = text[start - 1] + if not (char.isalnum() or char == "_"): + break + start -= 1 + + return text[start:pos] + + def show_completion(self): + if not self.completer: + return + + prefix = self.completion_prefix() + if not prefix: + self.completer.popup().hide() + return + + self.completer.setCompletionPrefix(prefix) + + if self.completer.completionCount() == 0: + self.completer.popup().hide() + return + + rect = self.cursorRect() + rect.setWidth( + self.completer.popup().sizeHintForColumn(0) + + self.completer.popup().verticalScrollBar().sizeHint().width() + ) + self.completer.complete(rect) + + def keyPressEvent(self, event): + if self.completer and self.completer.popup().isVisible(): + if event.key() in ( + Qt.Key_Enter, + Qt.Key_Return, + Qt.Key_Tab, + Qt.Key_Backtab, + ): + event.ignore() + return + + if event.key() == Qt.Key_Escape: + self.completer.popup().hide() + return + + if ( + event.key() == Qt.Key_Space + and event.modifiers() & Qt.ControlModifier + ): + self.show_completion() + return + + super().keyPressEvent(event) + + if self.completer: + self.show_completion() + +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_main() + self._build_menu() + self._build_toolbar() + self._build_docks() + self._build_statusbar() + + self._refresh_sidebars() + self.editor.setFocus() + + def _build_menu(self): + menu = self.menuBar() + + file_menu = menu.addMenu("File") + + export_action = file_menu.addAction("Export history to JSON...") + export_action.triggered.connect(self.export_history) + + import_action = file_menu.addAction("Import history from JSON...") + import_action.triggered.connect(self.import_history) + + file_menu.addSeparator() + + reset_action = file_menu.addAction("Reset Abacus") + reset_action.triggered.connect(self.reset_abacus) + + file_menu.addSeparator() + + exit_action = file_menu.addAction("Exit") + exit_action.triggered.connect(self.close) + + edit_menu = menu.addMenu("Edit") + clear_errors = edit_menu.addAction("Clear error highlighting") + clear_errors.triggered.connect(self.editor.clear_error_lines) + + 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() + self.highlighter = AbacusHighlighter(self.editor.document()) + + self.completion_model = QStringListModel() + self.completer = QCompleter(self.completion_model, self) + self.editor.set_completer(self.completer) + + 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.editor.clear_error_lines() + line_number = cursor.blockNumber() + 1 + self._execute(line, line_number) + + def run_document(self): + self.editor.clear_error_lines() + lines = self.editor.toPlainText().splitlines() + + executed = 0 + failed = 0 + + for line_index, line in enumerate(lines, start=1): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + + if self._execute(stripped, line_index): + executed += 1 + else: + failed += 1 + + if failed: + self.status.showMessage( + f"Executed {executed} command(s), {failed} error(s)" + ) + else: + self.status.showMessage(f"Executed {executed} command(s)") + + def _execute(self, text, line_number=None): + self.step += 1 + + try: + result = self.core.parse_input(text) + except Exception as exc: + self.step -= 1 + self.status.showMessage(f"Error: {exc}") + + if line_number is not None: + self.editor.set_error_lines({ + line_number: str(exc), + }) + + self.history_list.addItem( + f"ERROR line {line_number or '?'} {text} → {exc}" + ) + return False + + # 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 True + + 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}") + return True + + 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() + + functions = self.core.get_funcs() + + for key, value in functions.items(): + self.functions_list.addItem(f"{key} = {value}") + + names = list(self.core.get_vars().keys()) + list(functions.keys()) + self.completion_model.setStringList(sorted(set(names))) + + def export_history(self): + path, _ = QFileDialog.getSaveFileName( + self, + "Export history", + "abacus_history.json", + "JSON files (*.json)", + ) + if not path: + return + + data = { + "format": "abacus-history", + "version": 1, + "step": self.step, + "document": self.editor.toPlainText(), + "history": [ + self.history_list.item(i).text() + for i in range(self.history_list.count()) + ], + "results": self.results, + "graph_values": self.graph.values, + } + + try: + with open(path, "w", encoding="utf-8") as file: + json.dump( + data, + file, + ensure_ascii=False, + indent=2, + ) + except OSError as exc: + self.status.showMessage(f"Export error: {exc}") + return + + self.status.showMessage(f"History exported: {path}") + + def import_history(self): + path, _ = QFileDialog.getOpenFileName( + self, + "Import history", + "", + "JSON files (*.json)", + ) + if not path: + return + + try: + with open(path, "r", encoding="utf-8") as file: + data = json.load(file) + + if data.get("format") != "abacus-history": + raise ValueError("Not an Abacus history file") + + if data.get("version") != 1: + raise ValueError( + f"Unsupported history version: {data.get('version')}" + ) + + history = data.get("history", []) + results = data.get("results", []) + graph_values = data.get("graph_values", []) + document = data.get("document", "") + step = int(data.get("step", len(results))) + + if not isinstance(history, list): + raise ValueError("Invalid history data") + if not isinstance(results, list): + raise ValueError("Invalid results data") + if not isinstance(graph_values, list): + raise ValueError("Invalid graph data") + + except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc: + self.status.showMessage(f"Import error: {exc}") + return + + self.step = step + self.results = results + + self.history_list.clear() + self.history_list.addItems( + str(item) for item in history + ) + + self.results_table.setRowCount(0) + for item in self.results: + self._add_result_row( + item.get("step", ""), + item.get("expression", ""), + item.get("result", ""), + item.get("comment", ""), + ) + + self.graph.set_values( + [float(value) for value in graph_values] + if graph_values + else [] + ) + + if document: + self.editor.setPlainText(document) + + self.status.showMessage(f"History imported: {path}") + + 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.editor.clear_error_lines() + self._refresh_sidebars() + 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() diff --git a/abacus_pyside6.py b/abacus_pyside6.py new file mode 100644 index 0000000..2ee36b6 --- /dev/null +++ b/abacus_pyside6.py @@ -0,0 +1,354 @@ +import sys + +from PySide6.QtCore import Qt +from PySide6.QtGui import QFont +from PySide6.QtWidgets import ( + QApplication, + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QMainWindow, + QPushButton, + QSplitter, + QVBoxLayout, + QWidget, +) + +from abacus_core import AbacusCore + + +STYLE = """ +QMainWindow, QWidget { + background: #111318; + color: #e7e9ee; + font-family: "Segoe UI"; + font-size: 13px; +} + +QFrame#topbar { + background: #171a21; + border-bottom: 1px solid #292e38; +} + +QLabel#title { + font-size: 20px; + font-weight: 700; + color: #f5f7fb; +} + +QLabel#subtitle { + color: #858c99; +} + +QFrame#panel { + background: #171a21; + border: 1px solid #292e38; + border-radius: 12px; +} + +QLabel#panelTitle { + color: #9da5b3; + font-size: 12px; + font-weight: 700; +} + +QLineEdit#input { + background: #0d0f13; + border: 1px solid #343a46; + border-radius: 10px; + padding: 12px 14px; + color: #ffffff; + font-size: 16px; + selection-background-color: #3b82f6; +} + +QLineEdit#input:focus { + border: 1px solid #4b8cff; +} + +QPushButton { + background: #2864d7; + border: none; + border-radius: 9px; + padding: 11px 18px; + color: white; + font-weight: 700; +} + +QPushButton:hover { + background: #3475ef; +} + +QPushButton:pressed { + background: #1f55ba; +} + +QPushButton#secondary { + background: #242933; + color: #cbd1db; +} + +QPushButton#secondary:hover { + background: #2d333f; +} + +QListWidget { + background: transparent; + border: none; + outline: none; + padding: 4px; +} + +QListWidget::item { + padding: 8px 7px; + border-radius: 7px; +} + +QListWidget::item:hover { + background: #20242d; +} + +QListWidget::item:selected { + background: #202a3d; + color: #ffffff; +} + +QLabel#result { + color: #66d9a8; + font-size: 28px; + font-weight: 700; +} + +QLabel#status { + color: #7e8795; +} + +QSplitter::handle { + background: #111318; + width: 8px; +} +""" + + +class AbacusWindow(QMainWindow): + def __init__(self): + super().__init__() + + self.abacus = AbacusCore() + self.step = 0 + + self.setWindowTitle("Abacus") + self.resize(1100, 720) + self.setMinimumSize(850, 560) + + self._build_ui() + self._update_lists() + + def _build_ui(self): + root = QWidget() + root_layout = QVBoxLayout(root) + root_layout.setContentsMargins(18, 14, 18, 18) + root_layout.setSpacing(14) + + # Header + topbar = QFrame() + topbar.setObjectName("topbar") + top_layout = QHBoxLayout(topbar) + top_layout.setContentsMargins(4, 4, 4, 12) + + title_box = QVBoxLayout() + title_box.setSpacing(1) + + title = QLabel("Abacus") + title.setObjectName("title") + + subtitle = QLabel("Expression calculator") + subtitle.setObjectName("subtitle") + + title_box.addWidget(title) + title_box.addWidget(subtitle) + top_layout.addLayout(title_box) + top_layout.addStretch() + + self.status = QLabel("Ready") + self.status.setObjectName("status") + top_layout.addWidget(self.status) + + root_layout.addWidget(topbar) + + splitter = QSplitter(Qt.Horizontal) + splitter.setChildrenCollapsible(False) + + # Left: history + history_panel = self._make_panel("History") + history_layout = history_panel.layout() + + self.history = QListWidget() + self.history.setAlternatingRowColors(False) + history_layout.addWidget(self.history) + + clear_btn = QPushButton("Clear history") + clear_btn.setObjectName("secondary") + clear_btn.clicked.connect(self.history.clear) + history_layout.addWidget(clear_btn) + + splitter.addWidget(history_panel) + + # Center + center = QWidget() + center_layout = QVBoxLayout(center) + center_layout.setContentsMargins(0, 0, 0, 0) + center_layout.setSpacing(14) + + calc_panel = self._make_panel("Calculator") + calc_layout = calc_panel.layout() + + self.result = QLabel("0") + self.result.setObjectName("result") + self.result.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + calc_layout.addWidget(self.result) + + self.input = QLineEdit() + self.input.setObjectName("input") + self.input.setPlaceholderText("Enter expression, e.g. 2 + 3 * alpha") + self.input.returnPressed.connect(self.calculate) + calc_layout.addWidget(self.input) + + buttons = QHBoxLayout() + buttons.setSpacing(8) + + enter = QPushButton("Calculate") + enter.clicked.connect(self.calculate) + + clear = QPushButton("Clear") + clear.setObjectName("secondary") + clear.clicked.connect(self._clear_input) + + buttons.addWidget(enter) + buttons.addWidget(clear) + calc_layout.addLayout(buttons) + + center_layout.addWidget(calc_panel) + + help_panel = self._make_panel("Quick reference") + help_layout = help_panel.layout() + + help_text = QLabel( + "Variable: alpha = 10\n" + "Function: double: alpha * 2\n" + "Comment: 2 + 3 # test\n" + "Commands from the original console are available through the UI." + ) + help_text.setStyleSheet("color: #9da5b3; line-height: 1.5;") + help_text.setWordWrap(True) + help_layout.addWidget(help_text) + center_layout.addWidget(help_panel) + center_layout.addStretch() + + splitter.addWidget(center) + + # Right: variables/functions + right = QWidget() + right_layout = QVBoxLayout(right) + right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.setSpacing(14) + + var_panel = self._make_panel("Variables") + var_panel.layout().addWidget(QLabel("Stored values")) + self.variables = QListWidget() + var_panel.layout().addWidget(self.variables) + right_layout.addWidget(var_panel, 1) + + func_panel = self._make_panel("Functions") + func_panel.layout().addWidget(QLabel("Definitions")) + self.functions = QListWidget() + func_panel.layout().addWidget(self.functions) + right_layout.addWidget(func_panel, 1) + + splitter.addWidget(right) + + splitter.setSizes([260, 500, 280]) + root_layout.addWidget(splitter, 1) + + self.setCentralWidget(root) + + def _make_panel(self, title): + panel = QFrame() + panel.setObjectName("panel") + + layout = QVBoxLayout(panel) + layout.setContentsMargins(14, 13, 14, 14) + layout.setSpacing(9) + + label = QLabel(title) + label.setObjectName("panelTitle") + layout.addWidget(label) + + return panel + + def calculate(self): + text = self.input.text().strip() + if not text: + return + + self.step += 1 + + try: + result = self.abacus.parse_input(text) + except Exception as exc: + self.status.setText(f"Error: {exc}") + self.result.setText("Error") + return + + if not result: + self.input.clear() + self.status.setText("Comment / empty input") + return + + original, _, value, comment = result + value_text = str(value) + + line = f"{self.step}. {original} = {value_text}" + if comment: + line += f" #{comment}" + + self.history.addItem(line) + self.history.scrollToBottom() + + self.result.setText(value_text) + self.input.setText(value_text) + self.status.setText("Calculated") + self._update_lists() + + def _clear_input(self): + self.input.clear() + self.result.setText("0") + self.status.setText("Ready") + self.input.setFocus() + + def _update_lists(self): + self.variables.clear() + for key, value in self.abacus.get_vars().items(): + self.variables.addItem(f"{key} = {value}") + + self.functions.clear() + for key, value in self.abacus.get_funcs().items(): + self.functions.addItem(f"{key} = {value}") + + +def main(): + app = QApplication(sys.argv) + app.setStyle("Fusion") + app.setFont(QFont("Segoe UI", 10)) + app.setStyleSheet(STYLE) + + window = AbacusWindow() + window.show() + + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/abacus_pyside6_v2.py b/abacus_pyside6_v2.py new file mode 100644 index 0000000..1cac1a7 --- /dev/null +++ b/abacus_pyside6_v2.py @@ -0,0 +1,360 @@ +import sys + +from PySide6.QtCore import Qt +from PySide6.QtGui import QFont +from PySide6.QtWidgets import ( + QApplication, + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QMainWindow, + QPushButton, + QSplitter, + QVBoxLayout, + QWidget, +) + +from abacus_core import AbacusCore + + +STYLE = """ +QMainWindow, QWidget { + background: #111318; + color: #e7e9ee; + font-family: "Segoe UI"; + font-size: 13px; +} + +QFrame#topbar { + background: #111820; + border-bottom: 1px solid #292e38; +} + +QLabel#title { + font-size: 22px; + font-weight: 700; + color: #f5f7fb; +} + +QLabel#subtitle { + color: #858c99; +} + +QFrame#panel { + background: #111820; + border: 1px solid #202a36; + border-radius: 12px; +} + +QLabel#panelTitle { + color: #9da5b3; + font-size: 12px; + font-weight: 700; +} + +QLineEdit#input { + background: #0d0f13; + border: 1px solid #343a46; + border-radius: 10px; + padding: 12px 14px; + color: #ffffff; + font-size: 16px; + selection-background-color: #3b82f6; +} + +QLineEdit#input:focus { + border: 1px solid #4b8cff; +} + +QPushButton { + background: #2864d7; + border: none; + border-radius: 9px; + padding: 11px 18px; + color: white; + font-weight: 700; +} + +QPushButton:hover { + background: #3475ef; +} + +QPushButton:pressed { + background: #1f55ba; +} + +QPushButton#secondary { + background: #242933; + color: #cbd1db; +} + +QPushButton#secondary:hover { + background: #2d333f; +} + +QListWidget { + background: #0d131a; + border: 1px solid #202a36; + border-radius: 10px; + outline: none; + padding: 5px; +} + +QListWidget::item { + padding: 8px 7px; + border-radius: 7px; +} + +QListWidget::item:hover { + background: #20242d; +} + +QListWidget::item:selected { + background: #202a3d; + color: #ffffff; +} + +QLabel#result { + color: #66d9a8; + font-size: 28px; + font-weight: 700; +} + +QLabel#status { + color: #7e8795; +} + +QSplitter::handle { + background: #111318; + width: 8px; +} +""" + + +class AbacusWindow(QMainWindow): + def __init__(self): + super().__init__() + + self.abacus = AbacusCore() + self.step = 0 + + self.setWindowTitle("Abacus") + self.resize(1180, 760) + self.setMinimumSize(850, 560) + + self._build_ui() + self._update_lists() + + def _build_ui(self): + root = QWidget() + root_layout = QVBoxLayout(root) + root_layout.setContentsMargins(18, 14, 18, 18) + root_layout.setSpacing(14) + + # Header + topbar = QFrame() + topbar.setObjectName("topbar") + top_layout = QHBoxLayout(topbar) + top_layout.setContentsMargins(4, 4, 4, 12) + + title_box = QVBoxLayout() + title_box.setSpacing(1) + + title = QLabel("Abacus") + title.setObjectName("title") + + subtitle = QLabel("Expression calculator") + subtitle.setObjectName("subtitle") + + title_box.addWidget(title) + title_box.addWidget(subtitle) + top_layout.addLayout(title_box) + top_layout.addStretch() + + self.status = QLabel("Ready") + self.status.setObjectName("status") + top_layout.addWidget(self.status) + + root_layout.addWidget(topbar) + + splitter = QSplitter(Qt.Horizontal) + splitter.setChildrenCollapsible(False) + + # Left: history + history_panel = self._make_panel("History") + history_layout = history_panel.layout() + + self.history = QListWidget() + self.history.setAlternatingRowColors(False) + history_layout.addWidget(self.history) + + clear_btn = QPushButton("Clear history") + clear_btn.setObjectName("secondary") + clear_btn.clicked.connect(self.history.clear) + history_layout.addWidget(clear_btn) + + splitter.addWidget(history_panel) + + # Center + center = QWidget() + center_layout = QVBoxLayout(center) + center_layout.setContentsMargins(0, 0, 0, 0) + center_layout.setSpacing(14) + + calc_panel = self._make_panel("Calculator") + calc_layout = calc_panel.layout() + + self.result = QLabel("0") + self.result.setObjectName("result") + self.result.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + calc_layout.addWidget(self.result) + + self.input = QLineEdit() + self.input.setObjectName("input") + self.input.setPlaceholderText("Enter expression, e.g. 2 + 3 * alpha") + self.input.returnPressed.connect(self.calculate) + calc_layout.addWidget(self.input) + + buttons = QHBoxLayout() + buttons.setSpacing(8) + + enter = QPushButton("Calculate") + enter.clicked.connect(self.calculate) + + clear = QPushButton("Clear") + clear.setObjectName("secondary") + clear.clicked.connect(self._clear_input) + + buttons.addWidget(enter) + buttons.addWidget(clear) + calc_layout.addLayout(buttons) + + center_layout.addWidget(calc_panel) + + help_panel = self._make_panel("Quick reference") + help_layout = help_panel.layout() + + help_text = QLabel( + "Variable: alpha = 10\n" + "Function: double: alpha * 2\n" + "Comment: 2 + 3 # test\n" + "Commands from the original console are available through the UI." + ) + help_text.setStyleSheet("color: #9da5b3; line-height: 1.5;") + help_text.setWordWrap(True) + help_layout.addWidget(help_text) + center_layout.addWidget(help_panel) + center_layout.addStretch() + + splitter.addWidget(center) + + # Right: variables/functions + right = QWidget() + right_layout = QVBoxLayout(right) + right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.setSpacing(14) + + var_panel = self._make_panel("Variables") + var_panel.layout().addWidget(QLabel("Stored values")) + self.variables = QListWidget() + var_panel.layout().addWidget(self.variables) + right_layout.addWidget(var_panel, 1) + + func_panel = self._make_panel("Functions") + func_panel.layout().addWidget(QLabel("Definitions")) + self.functions = QListWidget() + func_panel.layout().addWidget(self.functions) + right_layout.addWidget(func_panel, 1) + + splitter.addWidget(right) + + splitter.setSizes([260, 500, 280]) + root_layout.addWidget(splitter, 1) + + self.setCentralWidget(root) + + def _make_panel(self, title): + panel = QFrame() + panel.setObjectName("panel") + + layout = QVBoxLayout(panel) + layout.setContentsMargins(14, 13, 14, 14) + layout.setSpacing(9) + + label = QLabel(title) + label.setObjectName("panelTitle") + layout.addWidget(label) + + return panel + + def calculate(self): + text = self.input.text().strip() + if not text: + return + + self.step += 1 + + try: + result = self.abacus.parse_input(text) + except Exception as exc: + self.step -= 1 + self.status.setText(f"Error: {exc}") + self.result.setText("Error") + return + + # Assignments return None from AbacusCore.parse_input(). + # Refresh the side panels BEFORE handling the result. + self._update_lists() + + if not result: + self.input.clear() + self.status.setText("Updated") + return + + original, _, value, comment = result + value_text = str(value) + + line = f"{self.step}. {original} = {value_text}" + if comment: + line += f" #{comment}" + + self.history.addItem(line) + self.history.scrollToBottom() + + self.result.setText(value_text) + self.input.setText(value_text) + self.status.setText("Calculated") + self._update_lists() + + def _clear_input(self): + self.input.clear() + self.result.setText("0") + self.status.setText("Ready") + self.input.setFocus() + + def _update_lists(self): + self.variables.clear() + for key, value in self.abacus.get_vars().items(): + self.variables.addItem(f"{key} = {value}") + + self.functions.clear() + for key, value in self.abacus.get_funcs().items(): + self.functions.addItem(f"{key} = {value}") + + +def main(): + app = QApplication(sys.argv) + app.setStyle("Fusion") + app.setFont(QFont("Segoe UI", 10)) + app.setStyleSheet(STYLE) + + window = AbacusWindow() + window.show() + + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/dieselfloater_edeka.json b/dieselfloater_edeka.json new file mode 100644 index 0000000..0bb910a --- /dev/null +++ b/dieselfloater_edeka.json @@ -0,0 +1,30 @@ +{ + "format": "abacus-history", + "version": 1, + "step": 4, + "document": "dieselpreis=1,99\nbasisfracht=200\npercent=20\nbasisfracht*(percent/100)*dieselpreis", + "variables": { + "result": 79.6, + "alphabet": 28, + "alpha": 1, + "beta": 2, + "dieselpreis": 1.99, + "basisfracht": 200, + "percent": 20 + }, + "functions": {}, + "history": [ + "4. basisfracht*(percent/100)*dieselpreis = 79.6" + ], + "results": [ + { + "step": 4, + "expression": "basisfracht*(percent/100)*dieselpreis", + "result": "79.6", + "comment": "" + } + ], + "graph_values": [ + 79.6 + ] +} \ No newline at end of file diff --git a/test.json b/test.json new file mode 100644 index 0000000..7135bea --- /dev/null +++ b/test.json @@ -0,0 +1,46 @@ +{ + "format": "abacus-history", + "version": 1, + "step": 7, + "document": "alpha=35\nbeta=22\nalpha+beta\ndouble:result*2\ndouble\ntriple:result*3\ntriple+double+22", + "variables": { + "result": 592, + "alphabet": 28, + "alpha": 35, + "beta": 22 + }, + "functions": { + "double": "result*2", + "triple": "result*3" + }, + "history": [ + "3. alpha+beta = 57", + "5. double = 114", + "7. triple+double+22 = 592" + ], + "results": [ + { + "step": 3, + "expression": "alpha+beta", + "result": "57", + "comment": "" + }, + { + "step": 5, + "expression": "double", + "result": "114", + "comment": "" + }, + { + "step": 7, + "expression": "triple+double+22", + "result": "592", + "comment": "" + } + ], + "graph_values": [ + 57.0, + 114.0, + 592.0 + ] +} \ No newline at end of file