worked on gui pyside6

working-on-gui
alex 2026-08-11 23:11:24 +02:00
parent b5584bf316
commit 3f18b0faae
11 changed files with 6000 additions and 0 deletions

2
.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
/.venv
/.idea

761
abacus_ide.py 100644
View File

@ -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()

View File

@ -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()

File diff suppressed because it is too large Load Diff

761
abacus_ide_fixed.py 100644
View File

@ -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()

View File

@ -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()

File diff suppressed because it is too large Load Diff

354
abacus_pyside6.py 100644
View File

@ -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()

View File

@ -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()

View File

@ -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
]
}

46
test.json 100644
View File

@ -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
]
}