diff --git a/.github/workflow/build.yml b/.github/workflow/build.yml new file mode 100644 index 0000000..03a5348 --- /dev/null +++ b/.github/workflow/build.yml @@ -0,0 +1,34 @@ +name: Build and Release Python App + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller + + - name: Build with PyInstaller + run: pyinstaller --onefile app.py + + - name: Upload Release Asset + uses: softprops/action-gh-release@v1 + with: + files: dist/app + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1fdf842 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,37 @@ +name: Build and Release Python App + +on: + push: + tags: + - 'v*' # Trigger on version tags like v1.0.0 + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller + pip install -r requirements.txt + + - name: Build with PyInstaller + run: pyinstaller --onefile main.py + + - name: Upload Release Asset + uses: softprops/action-gh-release@v1 + with: + files: dist/main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 071f934..b4eeda7 100644 --- a/README.md +++ b/README.md @@ -38,17 +38,45 @@ A web-based interface for compiling and running code in multiple languages local ## Usage -1. Run the Flask application: - ``` - python main.py - ``` -2. Open your web browser and navigate to: `http://localhost:5000` -3. Select the programming language you want to use -4. Enter your code in the editor or select one of the provided examples -5. Add compiler options if needed -6. Provide input for your program if required -7. Click "Compile & Run" to execute the code -8. View the output or any errors in the output panel +There are two ways to run this application: + +**1. As a Desktop Application (Recommended for Offline Use):** + + This method uses a PySide6 GUI to wrap the web interface, providing a standalone desktop experience. + + 1. Ensure all dependencies, including PySide6, are installed: + ```bash + pip install -r requirements.txt + ``` + 2. Run the PySide6 application: + ```bash + python app.py + ``` + This will open a window containing the compiler interface. The Flask server will be started and managed automatically in the background. + +**2. As a Web Application (Traditional Flask method):** + + If you prefer to run it as a standard web application accessible via a browser: + + 1. Ensure Flask is installed (it's included in `requirements.txt`): + ```bash + pip install -r requirements.txt + ``` + 2. Run the Flask application directly (ensure `main.py` is configured to run, or use `flask run`): + ```bash + flask run --host=0.0.0.0 --port=5000 + ``` + (You might need to set `FLASK_APP=main.py` as an environment variable if not running `python main.py` directly after uncommenting its `if __name__ == '__main__':` block) + 3. Open your web browser and navigate to: `http://localhost:5000` + +**Using the Interface (Applies to Both Methods):** + +1. Once the application is running (either as a desktop app or in your browser), select the programming language you want to use. +2. Enter your code in the editor or select one of the provided examples. +3. Add compiler options if needed. +4. Provide input for your program if required. +5. Click "Compile & Run" to execute the code. +6. View the output or any errors in the output panel. ## Features @@ -61,6 +89,19 @@ A web-based interface for compiling and running code in multiple languages local - Example code snippets for each language - Clean, responsive UI - Completely offline - no internet connection required +- **Code History:** Automatically saves your code snippets to a local SQLite database. You can view, load, and delete saved code via the history panel in the desktop application or through dedicated API endpoints if using the web version directly. + +### Code History Details + +- **Saving Code:** Code can be saved using the "Save" icon in the editor toolbar (Ctrl+S). A title is automatically generated from the first line of code, or you can be prompted for one (feature may vary by interface). +- **Viewing History:** + - **Desktop App:** A "Code History" panel is available as a dockable widget. It lists saved snippets with language, title, and timestamp. You can refresh this list, load code by double-clicking or using a context menu, and delete entries. + - **Web Interface (Direct):** History is accessible via API endpoints (`/api/history`). The HTML interface includes a basic (initially hidden) panel that can be populated via JavaScript if you are running the Flask app directly and not through the PySide6 wrapper. +- **Database Location:** The SQLite database file (`code_history.sqlite3`) is stored in a platform-specific user data directory: + - **Windows:** `%APPDATA%\OfflineCompiler\` (e.g., `C:\Users\\AppData\Roaming\OfflineCompiler\`) + - **Linux:** `$XDG_DATA_HOME/OfflineCompiler/` (typically `~/.local/share/OfflineCompiler/`) + - **macOS:** `~/Library/Application Support/OfflineCompiler/` (Note: `database.py` currently uses the Linux XDG path for macOS; this might be refined). +- **Deleting History:** Individual entries can be deleted from the history panel in the desktop app or via the API. ## Offline Mode diff --git a/__pycache__/database.cpython-312.pyc b/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..4fa1119 Binary files /dev/null and b/__pycache__/database.cpython-312.pyc differ diff --git a/__pycache__/database.cpython-313.pyc b/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..0c138bf Binary files /dev/null and b/__pycache__/database.cpython-313.pyc differ diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..f6685e3 Binary files /dev/null and b/__pycache__/main.cpython-312.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..836e7f1 Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..4024b0b --- /dev/null +++ b/app.py @@ -0,0 +1,208 @@ +import sys +import threading +import subprocess +import os # For path operations +from PySide6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QWidget, + QDockWidget, QListWidget, QListWidgetItem, QPushButton, + QHBoxLayout, QMessageBox, QMenu) +from PySide6.QtWebEngineWidgets import QWebEngineView +from PySide6.QtCore import QUrl, Qt, QTimer, QSize # Added QSize +from PySide6.QtGui import QAction # Added QAction for context menu + +# Assuming database.py is in the same directory +import database + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Offline Compiler App with History") + self.setGeometry(100, 100, 1000, 700) # Increased default size for history panel + + # Initialize database + self.db_session = next(database.get_db()) + database.create_db_tables() # Ensure tables are created + + self.setup_ui() + + self.flask_server_process = None + self.start_flask_server() + + QTimer.singleShot(2000, lambda: self.browser.setUrl(QUrl("http://localhost:5000"))) + + def setup_ui(self): + # Central Web View + self.browser = QWebEngineView() + + # History Dock Widget + self.history_dock = QDockWidget("Code History", self) + self.history_list_widget = QListWidget() + self.history_list_widget.itemDoubleClicked.connect(self.load_history_item) + + # Context menu for history items + self.history_list_widget.setContextMenuPolicy(Qt.CustomContextMenu) + self.history_list_widget.customContextMenuRequested.connect(self.show_history_context_menu) + + # Buttons for history management + self.refresh_history_button = QPushButton("Refresh History") + self.refresh_history_button.clicked.connect(self.populate_history_list) + + # Layout for history dock content + history_layout = QVBoxLayout() + history_layout.addWidget(self.refresh_history_button) + history_layout.addWidget(self.history_list_widget) + + history_container = QWidget() + history_container.setLayout(history_layout) + self.history_dock.setWidget(history_container) + self.addDockWidget(Qt.RightDockWidgetArea, self.history_dock) + + # Main layout + main_container = QWidget() # Central widget needs a container + main_layout = QHBoxLayout(main_container) # Use QHBoxLayout if browser is meant to be beside dock + main_layout.addWidget(self.browser) + # If dock is overlay or separate, browser can be set directly or in a QVBoxLayout + self.setCentralWidget(main_container) # Set the container as central widget + + self.populate_history_list() + + + def show_history_context_menu(self, position): + item = self.history_list_widget.itemAt(position) + if not item: + return + + context_menu = QMenu(self) + load_action = QAction("Load Code", self) + load_action.triggered.connect(lambda: self.load_history_item(item)) + context_menu.addAction(load_action) + + delete_action = QAction("Delete Entry", self) + delete_action.triggered.connect(lambda: self.delete_history_item_from_context_menu(item)) + context_menu.addAction(delete_action) + + context_menu.exec(self.history_list_widget.mapToGlobal(position)) + + def delete_history_item_from_context_menu(self, item): + entry_id = item.data(Qt.UserRole) # Retrieve stored ID + if entry_id is None: + QMessageBox.warning(self, "Error", "Could not find ID for this history item.") + return + + reply = QMessageBox.question(self, "Confirm Delete", + f"Are you sure you want to delete '{item.text()}'?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.Yes: + if database.delete_code_history_by_id(self.db_session, entry_id): + self.populate_history_list() # Refresh list + QMessageBox.information(self, "Success", "History entry deleted.") + # Optionally, notify the webview to clear if this code was loaded + # self.browser.page().runJavaScript("clearEditorIfMatchesHistoryId(...);") + else: + QMessageBox.warning(self, "Error", "Failed to delete history entry from database.") + + + def populate_history_list(self): + self.history_list_widget.clear() + try: + history_entries = database.get_all_code_history(self.db_session) + if not history_entries: + self.history_list_widget.addItem(QListWidgetItem("No history found.")) + return + + for entry in history_entries: + # Display title, language, and a bit of the timestamp + display_text = f"{entry.title} ({entry.language}) - {entry.timestamp.strftime('%Y-%m-%d %H:%M')}" + list_item = QListWidgetItem(display_text) + list_item.setData(Qt.UserRole, entry.id) # Store ID for later use + self.history_list_widget.addItem(list_item) + except Exception as e: + print(f"Error populating history list: {e}") + self.history_list_widget.addItem(QListWidgetItem("Error loading history.")) + + + def load_history_item(self, item): + entry_id = item.data(Qt.UserRole) + if entry_id is None: return # Should not happen if populated correctly + + entry = database.get_code_history_by_id(self.db_session, entry_id) + if entry: + # Use JavaScript to set code in CodeMirror editor in the web view + # This requires corresponding JavaScript functions in your web page + js_code = f""" + if (window.setCodeFromHost) {{ + window.setCodeFromHost('{entry.language}', `{entry.code.replace('`', '\\`')}`); + }} else {{ + console.warn('setCodeFromHost function not found in webview.'); + alert('Could not load code into editor: Communication function missing.'); + }} + """ + self.browser.page().runJavaScript(js_code) + else: + QMessageBox.warning(self, "Error", "Could not retrieve history item from database.") + + + def start_flask_server(self): + def run_server(): + env = {**os.environ, "FLASK_APP": "main.py", "FLASK_ENV": "development"} # FLASK_ENV for debug if needed + python_executable = sys.executable + if "pythonw.exe" in python_executable: + python_executable = python_executable.replace("pythonw.exe", "python.exe") + + # Ensure Flask uses the same Python interpreter if venvs are tricky + command = [python_executable, "-m", "flask", "run", "--host=0.0.0.0", "--port=5000"] + + try: + # For PyInstaller/frozen apps, main.py might not be directly accessible + # in the same way. Consider adjusting CWD or path if running frozen. + self.flask_server_process = subprocess.Popen(command, env=env) + self.flask_server_process.wait() + except Exception as e: + print(f"Failed to start Flask server: {e}") + # Show a message box to the user + QTimer.singleShot(0, lambda: QMessageBox.critical(self, "Server Error", f"Failed to start backend server: {e}")) + + + self.server_thread = threading.Thread(target=run_server) + self.server_thread.daemon = True + self.server_thread.start() + + def closeEvent(self, event): + print("Closing application...") + if self.flask_server_process and self.flask_server_process.poll() is None: + print("Terminating Flask server...") + self.flask_server_process.terminate() + try: + self.flask_server_process.wait(timeout=5) + print("Flask server terminated.") + except subprocess.TimeoutExpired: + print("Flask server did not terminate in time, killing...") + self.flask_server_process.kill() + self.flask_server_process.wait() + print("Flask server killed.") + + if hasattr(self, 'server_thread') and self.server_thread.is_alive(): + print("Joining server thread...") + self.server_thread.join(timeout=5) + if self.server_thread.is_alive(): print("Server thread did not join in time.") + else: print("Server thread joined.") + + if self.db_session: + self.db_session.close() + print("Database session closed.") + + event.accept() + +def main(): + QApplication.setAttribute(Qt.ApplicationAttribute.AA_ShareOpenGLContexts) # For WebEngine + app = QApplication(sys.argv) + + # Set app name for better OS integration (e.g., file paths) + QApplication.setApplicationName("OfflineCompiler") + QApplication.setOrganizationName("MyCompany") # Optional + + window = MainWindow() + window.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/app.spec b/app.spec new file mode 100644 index 0000000..49ac174 --- /dev/null +++ b/app.spec @@ -0,0 +1,44 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['app.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='app', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='app', +) diff --git a/app_run.log b/app_run.log new file mode 100644 index 0000000..c5813dd --- /dev/null +++ b/app_run.log @@ -0,0 +1,91 @@ + * Serving Flask app 'main.py' + * Debug mode: off +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.0.2:5000 +Press CTRL+C to quit +[4217:4368:0710/095240.873232:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4370:7:0710/095240.916472:ERROR:command_buffer_proxy_impl.cc(131)] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer. +127.0.0.1 - - [10/Jul/2025 09:52:40] "GET / HTTP/1.1" 200 - +[4217:4368:0710/095241.039717:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/codemirror/codemirror.min.css HTTP/1.1" 200 - +[4217:4368:0710/095241.060374:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.063472:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.066671:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/codemirror/theme/dracula.min.css HTTP/1.1" 200 - +[4217:4368:0710/095241.069781:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.074945:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.077536:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/codemirror/theme/monokai.min.css HTTP/1.1" 200 - +[4217:4368:0710/095241.080256:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/codemirror/theme/material.min.css HTTP/1.1" 200 - +[4217:4368:0710/095241.092950:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/codemirror/theme/solarized.min.css HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/codemirror/theme/nord.min.css HTTP/1.1" 200 - +[4217:4368:0710/095241.107378:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/codemirror/addon/fold/foldgutter.min.css HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/codemirror/addon/hint/show-hint.min.css HTTP/1.1" 200 - +[4217:4368:0710/095241.118982:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.121332:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.123006:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/codemirror/addon/dialog/dialog.min.css HTTP/1.1" 200 - +[4217:4368:0710/095241.125467:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.127635:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/codemirror/addon/search/matchesonscrollbar.min.css HTTP/1.1" 200 - +[4217:4368:0710/095241.132297:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/css/fontawesome/all.min.css HTTP/1.1" 404 - +[4217:4368:0710/095241.140111:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/mode/rust/rust.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/edit/closebrackets.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.144339:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/mode/python/python.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.150704:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/mode/clike/clike.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.154947:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/edit/closetag.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/codemirror.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.163972:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.169514:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/edit/matchbrackets.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/fold/foldcode.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.176974:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/fold/foldgutter.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.178710:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/fold/brace-fold.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.191255:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/fold/comment-fold.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.199572:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.202553:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.204863:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/hint/show-hint.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.215744:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/dialog/dialog.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.226732:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/search/jump-to-line.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/hint/anyword-hint.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/search/searchcursor.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.231121:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/search/search.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/search/match-highlighter.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.235948:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.239007:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.241908:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.243626:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/comment/comment.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/selection/active-line.min.js HTTP/1.1" 200 - +[4217:4368:0710/095241.254410:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +[4217:4368:0710/095241.256933:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/edit/trailingspace.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/keymap/sublime.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/codemirror/addon/display/placeholder.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/beautify/beautify.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/beautify/beautify-css.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /static/js/beautify/beautify-html.min.js HTTP/1.1" 200 - +js: Uncaught TypeError: e.defineSimpleMode is not a function +[4217:4368:0710/095241.485279:ERROR:socket_posix.cc(93)] CreatePlatformSocket() failed: Address family not supported by protocol (97) +127.0.0.1 - - [10/Jul/2025 09:52:41] "GET /favicon.ico HTTP/1.1" 404 - +127.0.0.1 - - [10/Jul/2025 09:52:45] "HEAD /static/css/codemirror/codemirror.min.css HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:45] "HEAD / HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:45] "POST /api/save_code HTTP/1.1" 200 - +127.0.0.1 - - [10/Jul/2025 09:52:45] "GET /api/history HTTP/1.1" 200 - diff --git a/build/app/base_library.zip b/build/app/base_library.zip new file mode 100644 index 0000000..1ce10e0 Binary files /dev/null and b/build/app/base_library.zip differ diff --git a/build/app/qt.conf b/build/app/qt.conf new file mode 100644 index 0000000..4196808 --- /dev/null +++ b/build/app/qt.conf @@ -0,0 +1,2 @@ +[Paths] +Prefix = . diff --git a/database.py b/database.py new file mode 100644 index 0000000..c66de77 --- /dev/null +++ b/database.py @@ -0,0 +1,90 @@ +import os +import datetime +from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from pathlib import Path + +# Determine the appropriate path for the database file +if os.name == 'nt': # Windows + app_data_path = Path(os.getenv('APPDATA', Path.home() / 'AppData' / 'Roaming')) +else: # Linux, macOS, etc. + app_data_path = Path(os.getenv('XDG_DATA_HOME', Path.home() / '.local' / 'share')) + +DATABASE_DIR = app_data_path / 'OfflineCompiler' +DATABASE_DIR.mkdir(parents=True, exist_ok=True) # Ensure the directory exists +DATABASE_URL = f"sqlite:///{DATABASE_DIR / 'code_history.sqlite3'}" + +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +# Define the CodeHistory model +class CodeHistory(Base): + __tablename__ = "code_history" + + id = Column(Integer, primary_key=True, index=True) + language = Column(String, index=True) + code = Column(Text) + timestamp = Column(DateTime, default=datetime.datetime.utcnow) + # Add a title or description field, perhaps derived from the first line of code or user input + title = Column(String, default="Untitled") + +# Create the database tables +def create_db_tables(): + Base.metadata.create_all(bind=engine) + +# --- Database Helper Functions --- + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +def add_code_history(db_session, language: str, code: str, title: str = None): + if not title: + # Generate a simple title from the first line of code if not provided + first_line = code.split('\n', 1)[0].strip() + title = first_line[:50] if first_line else "Untitled Snippet" + if len(first_line) > 50: + title += "..." + + db_entry = CodeHistory(language=language, code=code, title=title, timestamp=datetime.datetime.utcnow()) + db_session.add(db_entry) + db_session.commit() + db_session.refresh(db_entry) + return db_entry + +def get_all_code_history(db_session): + return db_session.query(CodeHistory).order_by(CodeHistory.timestamp.desc()).all() + +def get_code_history_by_id(db_session, entry_id: int): + return db_session.query(CodeHistory).filter(CodeHistory.id == entry_id).first() + +def delete_code_history_by_id(db_session, entry_id: int): + entry = db_session.query(CodeHistory).filter(CodeHistory.id == entry_id).first() + if entry: + db_session.delete(entry) + db_session.commit() + return True + return False + +if __name__ == "__main__": + # This will create the database and tables if they don't exist when database.py is run directly + print(f"Database will be created at: {DATABASE_DIR / 'code_history.sqlite3'}") + create_db_tables() + print("Database tables created (if they didn't exist).") + + # Example Usage (for testing) + # db = next(get_db()) + # add_code_history(db, "python", "print('Hello World')", "Hello Python") + # add_code_history(db, "rust", "fn main() {\n println!(\"Hello, Rust!\");\n}", "Hello Rust") + # history = get_all_code_history(db) + # for item in history: + # print(f"ID: {item.id}, Lang: {item.language}, Title: {item.title}, Time: {item.timestamp}") + # if history: + # delete_code_history_by_id(db, history[0].id) + # print(f"Deleted entry with ID: {history[0].id}") + # db.close() diff --git a/main.py b/main.py index b354ff3..192f5bc 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,118 @@ import os import subprocess import tempfile -from flask import Flask, request, render_template, jsonify, send_from_directory +from flask import Flask, request, render_template, jsonify, send_from_directory, abort +from sqlalchemy.orm import Session # For type hinting +import database # Import the database module app = Flask(__name__, static_url_path='/static', static_folder='static') +# Database session management for Flask routes +@app.before_request +def before_request(): + # This makes a new session available for each request + # and closes it when the request context ends. + # However, for Flask, it's often better to manage sessions per request explicitly + # or use a Flask extension like Flask-SQLAlchemy for more robust session handling. + # For this simple case, we'll get a session from database.py when needed. + pass + +@app.teardown_appcontext +def shutdown_session(exception=None): + # This would be used if we attached a session to g. + # database.SessionLocal.remove() # If using scoped_session + pass + + @app.route('/', methods=['GET']) def index(): + # Ensure database tables are created if they don't exist when app starts + # This is a good place if not running database.py directly earlier + # Moved to app.py or a startup script for PySide6 app to avoid issues with multiple initializations + # database.create_db_tables() return render_template('index.html') +# --- API Routes for Code History --- + +@app.route('/api/save_code', methods=['POST']) +def save_code_route(): + data = request.get_json() + if not data or 'language' not in data or 'code' not in data: + return jsonify({"success": False, "error": "Missing language or code"}), 400 + + language = data['language'] + code = data['code'] + title = data.get('title') # Optional title from frontend + + db_session = next(database.get_db()) + try: + entry = database.add_code_history(db_session, language, code, title) + return jsonify({"success": True, "id": entry.id, "title": entry.title, "message": "Code saved successfully."}) + except Exception as e: + db_session.rollback() + return jsonify({"success": False, "error": str(e)}), 500 + finally: + db_session.close() + +@app.route('/api/history', methods=['GET']) +def get_history_route(): + db_session = next(database.get_db()) + try: + history_entries = database.get_all_code_history(db_session) + # Convert SQLAlchemy objects to dictionaries for JSON serialization + history_list = [ + { + "id": entry.id, + "language": entry.language, + "code": entry.code, # Consider if sending full code always is okay, or just snippet/title + "title": entry.title, + "timestamp": entry.timestamp.isoformat() + } for entry in history_entries + ] + return jsonify({"success": True, "history": history_list}) + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + finally: + db_session.close() + +@app.route('/api/history/', methods=['GET']) +def get_history_entry_route(entry_id): + db_session = next(database.get_db()) + try: + entry = database.get_code_history_by_id(db_session, entry_id) + if entry: + return jsonify({ + "success": True, + "id": entry.id, + "language": entry.language, + "code": entry.code, + "title": entry.title, + "timestamp": entry.timestamp.isoformat() + }) + else: + return jsonify({"success": False, "error": "Entry not found"}), 404 + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 500 + finally: + db_session.close() + + +@app.route('/api/history/', methods=['DELETE']) +def delete_history_entry_route(entry_id): + db_session = next(database.get_db()) + try: + success = database.delete_code_history_by_id(db_session, entry_id) + if success: + return jsonify({"success": True, "message": "Entry deleted successfully."}) + else: + return jsonify({"success": False, "error": "Entry not found or could not be deleted."}), 404 + except Exception as e: + db_session.rollback() + return jsonify({"success": False, "error": str(e)}), 500 + finally: + db_session.close() + + @app.route('/compile', methods=['POST']) def compile_and_run(): code = request.form.get('code', '') @@ -169,5 +273,12 @@ def compile_and_run(): "runtime_error": "Program execution timed out (limit: 5 seconds)" }) -if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5000) +# The Flask app instance +# To run the app, use the command: flask run + +# To run with debug mode and specific host/port: +# flask run --debug --host=0.0.0.0 --port=5000 + +# The following block is commented out to prevent auto-run when imported +# if __name__ == '__main__': +# app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/requirements.txt b/requirements.txt index 0da3845..8073ad7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ flask==2.3.3 +PySide6 +SQLAlchemy diff --git a/templates/index.html b/templates/index.html index aaf66b3..7ba5bc2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -20,49 +20,56 @@ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; - max-width: 1200px; + /* max-width: 1200px; */ /* Allow full width */ margin: 0 auto; - padding: 20px; + padding: 10px; background-color: #f5f5f5; + display: flex; + flex-direction: column; + height: 100vh; /* Full viewport height */ + box-sizing: border-box; } h1 { text-align: center; - margin-bottom: 20px; + margin-bottom: 10px; color: #333; + font-size: 1.8em; } - .container { + .main-content-area { /* Wraps controls, editor, output */ display: flex; flex-direction: column; - gap: 20px; + flex-grow: 1; + overflow: hidden; + } + .container { /* Holds editor and output primarily */ + display: flex; + flex-direction: column; + gap: 10px; + flex-grow: 1; + overflow: hidden; } .editor-container { display: flex; - gap: 20px; - height: 70vh; + gap: 10px; + flex-grow: 1; /* Takes available vertical space */ + overflow: hidden; /* Children manage their own scroll */ } .code-section, .output-section { flex: 1; display: flex; flex-direction: column; + overflow: hidden; /* Children will scroll */ } .section-title { font-weight: bold; margin-bottom: 5px; + padding: 5px; + background-color: #e9e9e9; + border-bottom: 1px solid #ddd; } - #code-editor { - width: 100%; - height: 100%; - border: 1px solid #ccc; - border-radius: 4px; - padding: 10px; - font-family: 'Consolas', 'Courier New', monospace; - font-size: 14px; - resize: none; - background-color: #fff; - } + /* textarea#code-editor is replaced by CodeMirror */ .CodeMirror { - height: auto; - min-height: 300px; + flex-grow: 1; /* Makes CodeMirror fill available vertical space in its parent */ border: 1px solid #ddd; border-radius: 4px; font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; @@ -80,6 +87,10 @@ border-right: 1px solid #ddd; background-color: #f7f7f7; } + .CodeMirror-scroll { /* Ensure CM scroll takes full height within its flex container */ + min-height: 100px; /* Minimum reasonable height */ + height: 100%; + } .CodeMirror-linenumber { color: #999; padding: 0 8px; @@ -97,8 +108,7 @@ background: rgba(255, 255, 255, 0.1); } #output { - width: 100%; - height: 100%; + flex-grow: 1; /* Allows output to fill space */ border: 1px solid #ccc; border-radius: 4px; padding: 10px; @@ -107,37 +117,45 @@ background-color: #fff; overflow-y: auto; white-space: pre-wrap; + min-height: 100px; /* Minimum height for output */ } - .controls { + .controls { /* Holds status and compile button */ display: flex; justify-content: space-between; align-items: center; + padding: 5px 0; /* Reduced padding */ } - .language-options { + .language-options { /* Holds compiler options input */ display: flex; gap: 10px; - margin-bottom: 10px; + margin-bottom: 5px; /* Reduced margin */ align-items: center; } - .language-options select, .language-options input { - padding: 8px; + .language-options label { + font-size: 0.9em; + } + .language-options input { + padding: 6px; /* Reduced padding */ border: 1px solid #ccc; border-radius: 4px; font-size: 14px; + flex-grow: 1; } - button { - padding: 10px 20px; + button { /* General button styling */ + padding: 8px 15px; /* Slightly smaller buttons */ background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; - font-size: 16px; + font-size: 14px; /* Smaller font */ transition: background-color 0.3s; } button:hover { background-color: #45a049; } + #compile-btn { /* Specific styling if needed */ + } .error { color: #ff0000; } @@ -148,19 +166,22 @@ font-style: italic; } .examples { - margin-top: 20px; + margin-top: 5px; /* Reduced margin */ + margin-bottom: 5px; } .example-btn { background-color: #2196F3; - margin-right: 10px; - margin-bottom: 10px; + margin-right: 5px; /* Reduced margin */ + margin-bottom: 5px; + padding: 6px 10px; /* Smaller example buttons */ + font-size: 0.85em; } .example-btn:hover { background-color: #0b7dda; } .language-tabs { display: flex; - margin-bottom: 10px; + margin-bottom: 5px; /* Reduced margin */ } .language-tab { padding: 8px 16px; @@ -184,21 +205,24 @@ padding: 5px; background-color: #f0f0f0; border: 1px solid #ddd; - border-bottom: none; + border-bottom: none; /* CM will have top border */ border-radius: 4px 4px 0 0; } .editor-toolbar-group { display: flex; gap: 5px; } - .toolbar-btn { - padding: 5px 10px; + .toolbar-btn { /* Specific for toolbar buttons */ + padding: 5px 8px; /* Smaller toolbar buttons */ background-color: transparent; color: #333; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; - font-size: 14px; + font-size: 12px; /* Smaller font for toolbar */ + } + .toolbar-btn i { /* Ensure icons are not too large */ + font-size: 1em; } .toolbar-btn:hover { background-color: #e0e0e0; @@ -207,8 +231,90 @@ padding: 5px; border: 1px solid #ccc; border-radius: 3px; + font-size: 12px; /* Smaller font for select */ + } + #program-input { /* Styling for the program input textarea */ + /* height: 100px; */ /* Let flexbox manage height */ + flex-basis: 100px; /* Initial height, can grow/shrink if code-section is flex */ + flex-shrink: 0; /* Don't shrink if not enough space initially */ + width: calc(100% - 22px); /* Account for padding and border */ + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px; + font-family: 'Consolas', 'Courier New', monospace; font-size: 14px; + resize: vertical; /* Allow vertical resize only */ + background-color: #fff; + margin-top: 5px; /* Space from code editor's title */ + } + + /* History Panel Specific Styles */ + #history-panel { + position: fixed; /* Or absolute if body is relative */ + top: 10px; /* Align with top padding */ + right: 10px; + width: 300px; /* Adjust as needed */ + height: calc(100vh - 20px); /* Full height minus padding */ + background: #f9f9f9; + border-left: 1px solid #ccc; + box-shadow: -2px 0 5px rgba(0,0,0,0.1); + z-index: 1000; + display: flex; + flex-direction: column; + padding: 10px; + box-sizing: border-box; + } + #history-panel h3 { + margin-top: 0; + margin-bottom: 10px; + font-size: 1.2em; + border-bottom: 1px solid #eee; + padding-bottom: 5px; + } + #history-list { + list-style-type: none; + padding: 0; + margin: 0; + flex-grow: 1; + overflow-y: auto; + border: 1px solid #ddd; + background: white; } + #history-list li { + padding: 8px 10px; + border-bottom: 1px solid #eee; + cursor: pointer; + font-size: 0.9em; + } + #history-list li:hover { + background-color: #e6f7ff; + } + #history-list li .history-lang { + font-weight: bold; + color: #007bff; + margin-right: 5px; + } + #history-list li .history-timestamp { + font-size: 0.8em; + color: #777; + display: block; + } + #history-list li .delete-history-btn { + float: right; + color: #ff4d4d; + background: none; + border: none; + padding: 0 5px; + cursor: pointer; + font-size: 1.1em; + } + #history-list li .delete-history-btn:hover { + color: #cc0000; + } + #history-panel-toggle-btn { /* Button to toggle history panel visibility if not using PySide6 dock */ + /* Style if you add a button to hide/show this panel from HTML itself */ + } + .split-view { display: flex; gap: 10px; @@ -266,123 +372,119 @@ -

Local Code Compiler

-
-
Rust
-
Python
-
C
-
C++
-
- -
-
- - - - - - - - -
-
- - - +

Local Code Compiler

+ + + + +
+
+
+
Rust
+
Python
+
C
+
C++
-
- -
-
-
Editor Settings
-
- - -
-
- - -
-
- - -
-
- - + +
+
+ + + + +
-
- - +
+ + +
-
-
Keyboard Shortcuts
-
-
Ctrl+Space: Autocomplete
-
Ctrl+F: Find
-
Ctrl+H: Replace
-
Ctrl+G: Go to Line
-
Ctrl+/: Toggle Comment
-
Alt+F: Format Code
-
Ctrl+[: Indent
-
Ctrl+]: Outdent
+ +
+ +
+
Editor Settings
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
-
- -
- - -
- -
- - - -
- -
-
-
Code:
- -
Program Input:
- + +
+ + +
+ +
+ + + +
+ +
+
+
Code Editor
+ +
Program Input
+ +
+
+
Output
+
Output will appear here...
+
-
-
Output:
-
+
+
Ready
+
-
-
Ready
- -
-
+
+ + - - @@ -402,465 +504,413 @@

Local Code Compiler

- -