Source code for hyloa.gui.command_window

# This file is part of HYLOA - HYsteresis LOop Analyzer.
# Copyright (C) 2024 Francesco Zeno Costanzo

# HYLOA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# HYLOA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with HYLOA. If not, see <https://www.gnu.org/licenses/>.


"""
Code to manage the window where you can
run code to make changes to the data
"""

import io
import sys
import numpy as np
from PyQt5.QtWidgets import (
    QWidget, QVBoxLayout, QLabel, QTextEdit
)
from PyQt5.QtCore import Qt

from scipy.special import *
from scipy.optimize import *
import matplotlib.pyplot as plt

[docs] class ShellEditor(QTextEdit): ''' Class for wrinting in the shell ''' def __init__(self, parent=None): super().__init__(parent) self.setFontFamily("Courier") self.setFontPointSize(10) self.setLineWrapMode(QTextEdit.NoWrap) self.setText(">>> ")
[docs] def keyPressEvent(self, event): ''' Function to handle events from key pressing ''' cursor = self.textCursor() cursor_pos = cursor.position() prompt_pos = self.toPlainText().rfind(">>> ") + 4 # Protection against Backspace and Delete on the prompt if cursor_pos <= prompt_pos and event.key() in (Qt.Key_Backspace, Qt.Key_Delete): return # Block left arrow cursor movement before prompt if cursor_pos <= prompt_pos and event.key() == Qt.Key_Left: return # If I type before the prompt, I go back to the end of the text if cursor_pos < prompt_pos: cursor.setPosition(len(self.toPlainText())) self.setTextCursor(cursor) super().keyPressEvent(event)
[docs] def mousePressEvent(self, event): ''' Function to handle events from mouse pressing ''' cursor = self.cursorForPosition(event.pos()) cursor_pos = cursor.position() prompt_pos = self.toPlainText().rfind(">>> ") + 4 if cursor_pos < prompt_pos: # Force cursor after prompt cursor.setPosition(len(self.toPlainText())) self.setTextCursor(cursor) else: super().mousePressEvent(event)
[docs] class CommandWindow(QWidget): ''' Class for all the window, for comand history navigation and evaluation ''' def __init__(self, app_instance): super().__init__() self.app_instance = app_instance self.dataframes = app_instance.dataframes self.fit_results = app_instance.fit_results self.logger = app_instance.logger self.setWindowTitle("Python shell") layout = QVBoxLayout(self) layout.addWidget(QLabel("Only in-line command", self)) self.shell_text = ShellEditor(self) layout.addWidget(self.shell_text) self.local_vars = self._initialize_local_vars() self.command_history = [] self.history_index = -1 self.shell_text.installEventFilter(self) self.shell_text.moveCursor(self.shell_text.textCursor().End) def _initialize_local_vars(self): ''' Function to initialize the varibales konwn by the shell ''' local_vars = {} for idx, df in enumerate(self.dataframes): for column in df.columns: local_vars[column] = df[column].astype(float).values local_vars.update(self.fit_results) return local_vars
[docs] def refresh_variables(self): ''' Function to update the varibales konwn by the shell ''' self.local_vars.clear() for idx, df in enumerate(self.app_instance.dataframes): for column in df.columns: self.local_vars[column] = df[column].astype(float).values self.local_vars.update(self.app_instance.fit_results)
[docs] def eventFilter(self, obj, event): ''' Handle switch for navigation in command's history or execute command ''' if obj is self.shell_text: if event.type() == event.KeyPress: key = event.key() if key == Qt.Key_Return: self.execute_command() return True elif key == Qt.Key_Up: self.navigate_history(-1) return True elif key == Qt.Key_Down: self.navigate_history(1) return True return super().eventFilter(obj, event)
[docs] def execute_command(self): ''' Function that execute the command ''' text = self.shell_text.toPlainText() last_prompt = text.rfind(">>> ") command = text[last_prompt + 4:].strip() if not command: self.shell_text.append(">>> ") return self.command_history.append(command) self.history_index = len(self.command_history) if self.logger: self.logger.info(f"Execution of the command: {command}") old_stdout, old_stderr = sys.stdout, sys.stderr sys.stdout = output_capture = io.StringIO() sys.stderr = output_capture try: # First try to execute as expression try: code = compile(command, "<input>", "eval") res = eval(code, globals(), self.local_vars) if res is not None: print(repr(res)) self.local_vars["_"] = res except SyntaxError: # If not possible, execute as statement exec(command, globals(), self.local_vars) output = output_capture.getvalue() except Exception as e: output = f"Error: {str(e)}\n" finally: sys.stdout, sys.stderr = old_stdout, old_stderr # Update dataframes with modified variables for idx, df in enumerate(self.dataframes): for column in df.columns: if column in self.local_vars: modified_array = self.local_vars[column] if not np.array_equal(df[column].values, modified_array): df[column] = modified_array self.shell_text.append(output) self.shell_text.append(">>> ") self.shell_text.moveCursor(self.shell_text.textCursor().End)
[docs] def navigate_history(self, direction): ''' Function for navigating in all command's history ''' if not self.command_history: return self.history_index += direction self.history_index = max(0, min(self.history_index, len(self.command_history) - 1)) command = self.command_history[self.history_index] text = self.shell_text.toPlainText() last_prompt = text.rfind(">>> ") new_text = text[:last_prompt + 4] + command self.shell_text.setText(new_text) self.shell_text.moveCursor(self.shell_text.textCursor().End)
[docs] def append_text(self, text): cursor = self.shell_text.textCursor() cursor.movePosition(cursor.End) cursor.insertText(text) self.shell_text.setTextCursor(cursor) self.shell_text.ensureCursorVisible()