Skip to content
Merged
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
8 changes: 0 additions & 8 deletions .claude/settings.local.json

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,4 @@ dmypy.json

#backup files
.bkp
/.claude/
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "pyx2cscope"
version = "0.6.0"
version = "0.6.1"
description = "python implementation of X2Cscope"
authors = [
"Yash Agarwal",
Expand Down
4 changes: 2 additions & 2 deletions pyx2cscope/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""This module contains the pyx2cscope package.

Version: 0.6.0
Version: 0.6.1
"""

import logging

__version__ = "0.6.0"
__version__ = "0.6.1"

def set_logger(
level: int = logging.ERROR,
Expand Down
43 changes: 29 additions & 14 deletions pyx2cscope/gui/qt/tabs/scripting_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
from pyx2cscope.gui.qt.models.app_state import AppState




class ScriptHelpDialog(QDialog):
"""Dialog showing help for writing scripts."""

Expand Down Expand Up @@ -204,11 +206,12 @@ def _setup_ui(self): # noqa: PLR0915
self._browse_btn.clicked.connect(self._on_browse_clicked)
script_layout.addWidget(self._browse_btn)

# Edit with IDLE button
self._edit_btn = QPushButton("Edit (IDLE)")
# Edit button
self._edit_btn = QPushButton("Edit Script")
self._edit_btn.setFixedWidth(90)
self._edit_btn.clicked.connect(self._on_edit_clicked)
self._edit_btn.setEnabled(False)
self._edit_btn.setToolTip("Open script in text editor")
script_layout.addWidget(self._edit_btn)

# Help button
Expand Down Expand Up @@ -387,6 +390,25 @@ def _on_help_clicked(self):
dialog = ScriptHelpDialog(self)
dialog.exec_()

def _open_with_system_editor(self, script_path):
"""Open script with the system's default editor or associated application.

Uses OS-specific methods to open the file with the default application
associated with .py files.

Args:
script_path: Path to the script file to open.
"""
if sys.platform == "win32":
# Windows: Use os.startfile to open with associated application
os.startfile(script_path)
elif sys.platform == "darwin":
# macOS: Use 'open' command
subprocess.Popen(['open', script_path])
else:
# Linux: Use xdg-open to respect file associations
subprocess.Popen(['xdg-open', script_path])

def _on_browse_clicked(self):
"""Handle browse button click."""
# Get last directory from settings
Expand Down Expand Up @@ -414,23 +436,16 @@ def _on_browse_clicked(self):
self._log_path_edit.setText(self._log_file_path)

def _on_edit_clicked(self):
"""Open the script in IDLE."""
"""Open the script in the system's default editor."""
if not self._script_path:
return

try:
# Try to open with IDLE
if sys.platform == "win32":
# On Windows, use pythonw to avoid console window
subprocess.Popen(
[sys.executable, "-m", "idlelib.idle", self._script_path],
creationflags=subprocess.CREATE_NO_WINDOW,
)
else:
subprocess.Popen([sys.executable, "-m", "idlelib.idle", self._script_path])
self._log_message(f"Opened {os.path.basename(self._script_path)} in IDLE")
# Open with system default application for .py files
self._open_with_system_editor(self._script_path)
self._log_message(f"Opened {os.path.basename(self._script_path)} in default editor")
except Exception as e:
self._log_message(f"Error opening IDLE: {e}")
self._log_message(f"Error opening editor: {e}")

def _on_execute_clicked(self):
"""Execute the selected script."""
Expand Down
27 changes: 27 additions & 0 deletions pyx2cscope/gui/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""
import logging
import os
import socket
import webbrowser

import serial.tools.list_ports
Expand Down Expand Up @@ -37,6 +38,7 @@ def create_app():

app.add_url_rule("/", view_func=index)
app.add_url_rule("/serial-ports", view_func=list_serial_ports)
app.add_url_rule("/local-ips", view_func=get_local_ips)
app.add_url_rule("/connect", view_func=connect, methods=["POST"])
app.add_url_rule("/disconnect", view_func=disconnect)
app.add_url_rule("/is-connected", view_func=is_connected)
Expand Down Expand Up @@ -65,6 +67,31 @@ def list_serial_ports():
return jsonify([port.device for port in ports])


def get_local_ips():
"""Return a list of local IP addresses for this host.

call {server_url}/local-ips to execute.
"""
ips = []
try:
# Get hostname
hostname = socket.gethostname()
# Get all IP addresses for this hostname
for info in socket.getaddrinfo(hostname, None):
ip = info[4][0]
# Filter IPv4 addresses and exclude localhost
if ':' not in ip and ip != '127.0.0.1' and ip not in ips:
ips.append(ip)
except Exception:
pass

# If no IPs found, add default
if not ips:
ips = ['0.0.0.0']

return jsonify({"ips": ips})


def connect():
"""Connect pyX2CScope using arguments coming from the web.

Expand Down
28 changes: 25 additions & 3 deletions pyx2cscope/gui/web/static/js/dashboard_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ function updateDashboardVariable(varName, value) {
value: rawValue
});
} else {
fetch('/dashboard-view/update', {
fetch('/dashboard/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ variable: varName, value: rawValue })
Expand Down Expand Up @@ -951,7 +951,7 @@ function stopDashboardDrag() {
}

function saveDashboardLayout() {
fetch('/dashboard-view/save-layout', {
fetch('/dashboard/save-layout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dashboardWidgets)
Expand All @@ -967,14 +967,25 @@ function saveDashboardLayout() {
}

function loadDashboardLayout() {
fetch('/dashboard-view/load-layout')
fetch('/dashboard/load-layout')
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
// Unregister old variables
removeAllDashboardVariables();
// Destroy all existing chart instances before clearing
for (const id in dashboardCharts) {
if (dashboardCharts[id]) {
dashboardCharts[id].destroy();
delete dashboardCharts[id];
}
}
// Load new layout
dashboardWidgets = data.layout;
widgetIdCounter = Math.max(...dashboardWidgets.map(w => w.id), 0) + 1;
// Clear canvas
document.getElementById('dashboardCanvas').innerHTML = '';
// Render new widgets
dashboardWidgets.forEach(w => renderDashboardWidget(w));
registerAllDashboardVariables();
syncScopeControlToBackend();
Expand Down Expand Up @@ -1031,10 +1042,21 @@ function handleDashboardFileImport(e) {
const reader = new FileReader();
reader.onload = (event) => {
try {
// Unregister old variables
removeAllDashboardVariables();
// Destroy all existing chart instances before clearing
for (const id in dashboardCharts) {
if (dashboardCharts[id]) {
dashboardCharts[id].destroy();
delete dashboardCharts[id];
}
}
// Parse and load new layout
dashboardWidgets = JSON.parse(event.target.result);
widgetIdCounter = Math.max(...dashboardWidgets.map(w => w.id), 0) + 1;
// Clear canvas
document.getElementById('dashboardCanvas').innerHTML = '';
// Render new widgets
dashboardWidgets.forEach(w => renderDashboardWidget(w));
registerAllDashboardVariables();
syncScopeControlToBackend();
Expand Down
104 changes: 89 additions & 15 deletions pyx2cscope/gui/web/static/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function connect(){
}

function disconnect(){
$.getJSON('/disconnect', function(data) {
$.getJSON('/disconnect', function() {
setConnectState(false);
});
}
Expand Down Expand Up @@ -131,26 +131,82 @@ function initSetupCard(){
}

function insertQRCode(link) {
qrCodeHtml = "<form id='ipForm'> \
<label for='ipAddress'>Enter local IP address:</label> \
<input type='text' id='ipAddress' name='ipAddress' placeholder='192.168.1.1'> \
<button id='updateButton'>Update</button> \
</form><div id='qrcode'></div>";
qrCodeHtml = `
<div class="mb-3">
<h6>Instructions</h6>
<ol class="small text-muted">
<li>Ensure your mobile device is connected to the same network as this host.</li>
<li>The server must be started with shared access: <code>--host 0.0.0.0</code></li>
<li>Select your host's IP address below or enter a custom one.</li>
<li>Scan the QR code with your mobile device to open the page.</li>
</ol>
</div>
<form id='ipForm'>
<div class="mb-3">
<label for='ipSelect' class="form-label">Select Host IP Address:</label>
<select id='ipSelect' class="form-select">
<option value="">Loading available IPs...</option>
</select>
</div>
<div class="mb-3">
<label for='ipAddress' class="form-label">Or enter custom IP:</label>
<input type='text' id='ipAddress' class="form-control" name='ipAddress' placeholder='192.168.1.1'>
</div>
<button id='updateButton' class="btn btn-primary">Generate QR Code</button>
</form>
<div id='qrcode' class='mt-3 text-center'></div>
`;

$('#x2cModalBody').empty();
$('#x2cModalBody').html(qrCodeHtml);

new QRCode(document.getElementById("qrcode"), "http://0.0.0.0:5000/" + link);
// Load available IP addresses
$.getJSON('/local-ips', function(data) {
const ipSelect = $('#ipSelect');

ipSelect.empty();
if (data.ips && data.ips.length > 0) {
data.ips.forEach(function(ip) {
ipSelect.append($('<option></option>').val(ip).text(ip));
});
// Generate initial QR code with first IP
new QRCode(document.getElementById("qrcode"), "http://" + data.ips[0] + ":5000/" + link);
} else {
ipSelect.append($('<option></option>').val('').text('No IPs found - enter manually'));
new QRCode(document.getElementById("qrcode"), "http://0.0.0.0:5000/" + link);
}
}).fail(function() {
// Fallback if endpoint doesn't exist
$('#ipSelect').empty().append($('<option></option>').val('0.0.0.0').text('0.0.0.0 (default)'));
new QRCode(document.getElementById("qrcode"), "http://0.0.0.0:5000/" + link);
});

$('#updateButton').click(function(e) {
e.preventDefault(); // Prevent the default form submit action
// Update QR code when IP is selected from dropdown
$(document).on('change', '#ipSelect', function() {
const selectedIp = $(this).val();
if (selectedIp) {
$("#qrcode").empty();
new QRCode(document.getElementById("qrcode"), "http://" + selectedIp + ":5000/" + link);
}
});

// Update QR code when custom IP is entered
$(document).on('click', '#updateButton', function(e) {
e.preventDefault();
var ipAddress = $('#ipAddress').val();
var ipFormat = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
if(ipFormat.test(ipAddress)) {
if (ipAddress && ipFormat.test(ipAddress)) {
$("#qrcode").empty();
new QRCode(document.getElementById("qrcode"), "http://" + ipAddress + ":5000/" + link);
} else {
} else if (ipAddress) {
alert('Invalid IP address format.');
} else {
// Use selected IP from dropdown
const selectedIp = $('#ipSelect').val();
if (selectedIp) {
$("#qrcode").empty();
new QRCode(document.getElementById("qrcode"), "http://" + selectedIp + ":5000/" + link);
}
}
});
}
Expand All @@ -159,30 +215,48 @@ function initQRCodes() {

$("#watchQRCode").on("click", function() {
$('#x2cModalTitle').html('WatchView - Scan QR Code');
insertQRCode("watch-view");
insertQRCode("watch");
$('#x2cModal').modal('show');
});
$("#scopeQRCode").on("click", function() {
$('#x2cModalTitle').html('ScopeView - Scan QR Code');
insertQRCode("scope-view");
insertQRCode("scope");
$('#x2cModal').modal('show');
});
$("#dashboardQRCode").on("click", function() {
$('#x2cModalTitle').html('Dashboard - Scan QR Code');
insertQRCode("dashboard-view");
insertQRCode("dashboard");
$('#x2cModal').modal('show');
});
$("#scriptQRCode").on("click", function() {
$('#x2cModalTitle').html('Script - Scan QR Code');
insertQRCode("script-view");
insertQRCode("scripting");
$('#x2cModal').modal('show');
});
$("#mainPageQRCode").on("click", function() {
$('#x2cModalTitle').html('Main Page - Scan QR Code');
insertQRCode("");
$('#x2cModal').modal('show');
});
}

function initHelpToggle() {
const helpToggle = document.getElementById('helpToggle');
const welcomeCard = document.getElementById('welcomeCard');

if (helpToggle && welcomeCard) {
helpToggle.addEventListener('click', function(e) {
e.preventDefault();
welcomeCard.classList.toggle('d-none');
});
}
}

$(document).ready(function() {
initSetupCard();
setInterfaceSetupFields();
initQRCodes();
initHelpToggle();

// Toggles for views
const toggleWatch = document.getElementById('toggleWatch');
Expand Down
Loading