Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflow/build.yml
Original file line number Diff line number Diff line change
@@ -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 }}
37 changes: 37 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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 }}
63 changes: 52 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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\<YourUser>\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

Expand Down
Binary file added __pycache__/database.cpython-312.pyc
Binary file not shown.
Binary file added __pycache__/database.cpython-313.pyc
Binary file not shown.
Binary file added __pycache__/main.cpython-312.pyc
Binary file not shown.
Binary file added __pycache__/main.cpython-313.pyc
Binary file not shown.
208 changes: 208 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -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()
Loading