Skip to content

Commit 55a4a2a

Browse files
X2CscopeEdras Pacola
andauthored
Bug fix 0.6.1 (#108)
* build include gui.resources folder * fixed dashboard load/save url * fixed import/load/save dashboard * fixed script editing on QT * v0.6.1 * fixed qr codes urls and included tests for it * added help card for web app * added tests for broken urls on links at Web App * added instructions for QR Codes, IP Addresses and connectivity * enhanced gitignore --------- Co-authored-by: Edras Pacola <edras.pacola@microchip.com>
1 parent 37201b9 commit 55a4a2a

15 files changed

Lines changed: 290 additions & 56 deletions

.claude/settings.local.json

Lines changed: 0 additions & 8 deletions
This file was deleted.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,4 @@ dmypy.json
142142

143143
#backup files
144144
.bkp
145+
/.claude/

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "pyx2cscope"
7-
version = "0.6.0"
7+
version = "0.6.1"
88
description = "python implementation of X2Cscope"
99
authors = [
1010
"Yash Agarwal",

pyx2cscope/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""This module contains the pyx2cscope package.
22
3-
Version: 0.6.0
3+
Version: 0.6.1
44
"""
55

66
import logging
77

8-
__version__ = "0.6.0"
8+
__version__ = "0.6.1"
99

1010
def set_logger(
1111
level: int = logging.ERROR,

pyx2cscope/gui/qt/tabs/scripting_tab.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
from pyx2cscope.gui.qt.models.app_state import AppState
3434

3535

36+
37+
3638
class ScriptHelpDialog(QDialog):
3739
"""Dialog showing help for writing scripts."""
3840

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

207-
# Edit with IDLE button
208-
self._edit_btn = QPushButton("Edit (IDLE)")
209+
# Edit button
210+
self._edit_btn = QPushButton("Edit Script")
209211
self._edit_btn.setFixedWidth(90)
210212
self._edit_btn.clicked.connect(self._on_edit_clicked)
211213
self._edit_btn.setEnabled(False)
214+
self._edit_btn.setToolTip("Open script in text editor")
212215
script_layout.addWidget(self._edit_btn)
213216

214217
# Help button
@@ -387,6 +390,25 @@ def _on_help_clicked(self):
387390
dialog = ScriptHelpDialog(self)
388391
dialog.exec_()
389392

393+
def _open_with_system_editor(self, script_path):
394+
"""Open script with the system's default editor or associated application.
395+
396+
Uses OS-specific methods to open the file with the default application
397+
associated with .py files.
398+
399+
Args:
400+
script_path: Path to the script file to open.
401+
"""
402+
if sys.platform == "win32":
403+
# Windows: Use os.startfile to open with associated application
404+
os.startfile(script_path)
405+
elif sys.platform == "darwin":
406+
# macOS: Use 'open' command
407+
subprocess.Popen(['open', script_path])
408+
else:
409+
# Linux: Use xdg-open to respect file associations
410+
subprocess.Popen(['xdg-open', script_path])
411+
390412
def _on_browse_clicked(self):
391413
"""Handle browse button click."""
392414
# Get last directory from settings
@@ -414,23 +436,16 @@ def _on_browse_clicked(self):
414436
self._log_path_edit.setText(self._log_file_path)
415437

416438
def _on_edit_clicked(self):
417-
"""Open the script in IDLE."""
439+
"""Open the script in the system's default editor."""
418440
if not self._script_path:
419441
return
420442

421443
try:
422-
# Try to open with IDLE
423-
if sys.platform == "win32":
424-
# On Windows, use pythonw to avoid console window
425-
subprocess.Popen(
426-
[sys.executable, "-m", "idlelib.idle", self._script_path],
427-
creationflags=subprocess.CREATE_NO_WINDOW,
428-
)
429-
else:
430-
subprocess.Popen([sys.executable, "-m", "idlelib.idle", self._script_path])
431-
self._log_message(f"Opened {os.path.basename(self._script_path)} in IDLE")
444+
# Open with system default application for .py files
445+
self._open_with_system_editor(self._script_path)
446+
self._log_message(f"Opened {os.path.basename(self._script_path)} in default editor")
432447
except Exception as e:
433-
self._log_message(f"Error opening IDLE: {e}")
448+
self._log_message(f"Error opening editor: {e}")
434449

435450
def _on_execute_clicked(self):
436451
"""Execute the selected script."""

pyx2cscope/gui/web/app.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66
import logging
77
import os
8+
import socket
89
import webbrowser
910

1011
import serial.tools.list_ports
@@ -37,6 +38,7 @@ def create_app():
3738

3839
app.add_url_rule("/", view_func=index)
3940
app.add_url_rule("/serial-ports", view_func=list_serial_ports)
41+
app.add_url_rule("/local-ips", view_func=get_local_ips)
4042
app.add_url_rule("/connect", view_func=connect, methods=["POST"])
4143
app.add_url_rule("/disconnect", view_func=disconnect)
4244
app.add_url_rule("/is-connected", view_func=is_connected)
@@ -65,6 +67,31 @@ def list_serial_ports():
6567
return jsonify([port.device for port in ports])
6668

6769

70+
def get_local_ips():
71+
"""Return a list of local IP addresses for this host.
72+
73+
call {server_url}/local-ips to execute.
74+
"""
75+
ips = []
76+
try:
77+
# Get hostname
78+
hostname = socket.gethostname()
79+
# Get all IP addresses for this hostname
80+
for info in socket.getaddrinfo(hostname, None):
81+
ip = info[4][0]
82+
# Filter IPv4 addresses and exclude localhost
83+
if ':' not in ip and ip != '127.0.0.1' and ip not in ips:
84+
ips.append(ip)
85+
except Exception:
86+
pass
87+
88+
# If no IPs found, add default
89+
if not ips:
90+
ips = ['0.0.0.0']
91+
92+
return jsonify({"ips": ips})
93+
94+
6895
def connect():
6996
"""Connect pyX2CScope using arguments coming from the web.
7097

pyx2cscope/gui/web/static/js/dashboard_view.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,7 @@ function updateDashboardVariable(varName, value) {
812812
value: rawValue
813813
});
814814
} else {
815-
fetch('/dashboard-view/update', {
815+
fetch('/dashboard/update', {
816816
method: 'POST',
817817
headers: { 'Content-Type': 'application/json' },
818818
body: JSON.stringify({ variable: varName, value: rawValue })
@@ -951,7 +951,7 @@ function stopDashboardDrag() {
951951
}
952952

953953
function saveDashboardLayout() {
954-
fetch('/dashboard-view/save-layout', {
954+
fetch('/dashboard/save-layout', {
955955
method: 'POST',
956956
headers: { 'Content-Type': 'application/json' },
957957
body: JSON.stringify(dashboardWidgets)
@@ -967,14 +967,25 @@ function saveDashboardLayout() {
967967
}
968968

969969
function loadDashboardLayout() {
970-
fetch('/dashboard-view/load-layout')
970+
fetch('/dashboard/load-layout')
971971
.then(r => r.json())
972972
.then(data => {
973973
if (data.status === 'success') {
974+
// Unregister old variables
974975
removeAllDashboardVariables();
976+
// Destroy all existing chart instances before clearing
977+
for (const id in dashboardCharts) {
978+
if (dashboardCharts[id]) {
979+
dashboardCharts[id].destroy();
980+
delete dashboardCharts[id];
981+
}
982+
}
983+
// Load new layout
975984
dashboardWidgets = data.layout;
976985
widgetIdCounter = Math.max(...dashboardWidgets.map(w => w.id), 0) + 1;
986+
// Clear canvas
977987
document.getElementById('dashboardCanvas').innerHTML = '';
988+
// Render new widgets
978989
dashboardWidgets.forEach(w => renderDashboardWidget(w));
979990
registerAllDashboardVariables();
980991
syncScopeControlToBackend();
@@ -1031,10 +1042,21 @@ function handleDashboardFileImport(e) {
10311042
const reader = new FileReader();
10321043
reader.onload = (event) => {
10331044
try {
1045+
// Unregister old variables
10341046
removeAllDashboardVariables();
1047+
// Destroy all existing chart instances before clearing
1048+
for (const id in dashboardCharts) {
1049+
if (dashboardCharts[id]) {
1050+
dashboardCharts[id].destroy();
1051+
delete dashboardCharts[id];
1052+
}
1053+
}
1054+
// Parse and load new layout
10351055
dashboardWidgets = JSON.parse(event.target.result);
10361056
widgetIdCounter = Math.max(...dashboardWidgets.map(w => w.id), 0) + 1;
1057+
// Clear canvas
10371058
document.getElementById('dashboardCanvas').innerHTML = '';
1059+
// Render new widgets
10381060
dashboardWidgets.forEach(w => renderDashboardWidget(w));
10391061
registerAllDashboardVariables();
10401062
syncScopeControlToBackend();

pyx2cscope/gui/web/static/js/script.js

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function connect(){
4545
}
4646

4747
function disconnect(){
48-
$.getJSON('/disconnect', function(data) {
48+
$.getJSON('/disconnect', function() {
4949
setConnectState(false);
5050
});
5151
}
@@ -131,26 +131,82 @@ function initSetupCard(){
131131
}
132132

133133
function insertQRCode(link) {
134-
qrCodeHtml = "<form id='ipForm'> \
135-
<label for='ipAddress'>Enter local IP address:</label> \
136-
<input type='text' id='ipAddress' name='ipAddress' placeholder='192.168.1.1'> \
137-
<button id='updateButton'>Update</button> \
138-
</form><div id='qrcode'></div>";
134+
qrCodeHtml = `
135+
<div class="mb-3">
136+
<h6>Instructions</h6>
137+
<ol class="small text-muted">
138+
<li>Ensure your mobile device is connected to the same network as this host.</li>
139+
<li>The server must be started with shared access: <code>--host 0.0.0.0</code></li>
140+
<li>Select your host's IP address below or enter a custom one.</li>
141+
<li>Scan the QR code with your mobile device to open the page.</li>
142+
</ol>
143+
</div>
144+
<form id='ipForm'>
145+
<div class="mb-3">
146+
<label for='ipSelect' class="form-label">Select Host IP Address:</label>
147+
<select id='ipSelect' class="form-select">
148+
<option value="">Loading available IPs...</option>
149+
</select>
150+
</div>
151+
<div class="mb-3">
152+
<label for='ipAddress' class="form-label">Or enter custom IP:</label>
153+
<input type='text' id='ipAddress' class="form-control" name='ipAddress' placeholder='192.168.1.1'>
154+
</div>
155+
<button id='updateButton' class="btn btn-primary">Generate QR Code</button>
156+
</form>
157+
<div id='qrcode' class='mt-3 text-center'></div>
158+
`;
139159

140160
$('#x2cModalBody').empty();
141161
$('#x2cModalBody').html(qrCodeHtml);
142162

143-
new QRCode(document.getElementById("qrcode"), "http://0.0.0.0:5000/" + link);
163+
// Load available IP addresses
164+
$.getJSON('/local-ips', function(data) {
165+
const ipSelect = $('#ipSelect');
166+
167+
ipSelect.empty();
168+
if (data.ips && data.ips.length > 0) {
169+
data.ips.forEach(function(ip) {
170+
ipSelect.append($('<option></option>').val(ip).text(ip));
171+
});
172+
// Generate initial QR code with first IP
173+
new QRCode(document.getElementById("qrcode"), "http://" + data.ips[0] + ":5000/" + link);
174+
} else {
175+
ipSelect.append($('<option></option>').val('').text('No IPs found - enter manually'));
176+
new QRCode(document.getElementById("qrcode"), "http://0.0.0.0:5000/" + link);
177+
}
178+
}).fail(function() {
179+
// Fallback if endpoint doesn't exist
180+
$('#ipSelect').empty().append($('<option></option>').val('0.0.0.0').text('0.0.0.0 (default)'));
181+
new QRCode(document.getElementById("qrcode"), "http://0.0.0.0:5000/" + link);
182+
});
144183

145-
$('#updateButton').click(function(e) {
146-
e.preventDefault(); // Prevent the default form submit action
184+
// Update QR code when IP is selected from dropdown
185+
$(document).on('change', '#ipSelect', function() {
186+
const selectedIp = $(this).val();
187+
if (selectedIp) {
188+
$("#qrcode").empty();
189+
new QRCode(document.getElementById("qrcode"), "http://" + selectedIp + ":5000/" + link);
190+
}
191+
});
192+
193+
// Update QR code when custom IP is entered
194+
$(document).on('click', '#updateButton', function(e) {
195+
e.preventDefault();
147196
var ipAddress = $('#ipAddress').val();
148197
var ipFormat = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
149-
if(ipFormat.test(ipAddress)) {
198+
if (ipAddress && ipFormat.test(ipAddress)) {
150199
$("#qrcode").empty();
151200
new QRCode(document.getElementById("qrcode"), "http://" + ipAddress + ":5000/" + link);
152-
} else {
201+
} else if (ipAddress) {
153202
alert('Invalid IP address format.');
203+
} else {
204+
// Use selected IP from dropdown
205+
const selectedIp = $('#ipSelect').val();
206+
if (selectedIp) {
207+
$("#qrcode").empty();
208+
new QRCode(document.getElementById("qrcode"), "http://" + selectedIp + ":5000/" + link);
209+
}
154210
}
155211
});
156212
}
@@ -159,30 +215,48 @@ function initQRCodes() {
159215

160216
$("#watchQRCode").on("click", function() {
161217
$('#x2cModalTitle').html('WatchView - Scan QR Code');
162-
insertQRCode("watch-view");
218+
insertQRCode("watch");
163219
$('#x2cModal').modal('show');
164220
});
165221
$("#scopeQRCode").on("click", function() {
166222
$('#x2cModalTitle').html('ScopeView - Scan QR Code');
167-
insertQRCode("scope-view");
223+
insertQRCode("scope");
168224
$('#x2cModal').modal('show');
169225
});
170226
$("#dashboardQRCode").on("click", function() {
171227
$('#x2cModalTitle').html('Dashboard - Scan QR Code');
172-
insertQRCode("dashboard-view");
228+
insertQRCode("dashboard");
173229
$('#x2cModal').modal('show');
174230
});
175231
$("#scriptQRCode").on("click", function() {
176232
$('#x2cModalTitle').html('Script - Scan QR Code');
177-
insertQRCode("script-view");
233+
insertQRCode("scripting");
234+
$('#x2cModal').modal('show');
235+
});
236+
$("#mainPageQRCode").on("click", function() {
237+
$('#x2cModalTitle').html('Main Page - Scan QR Code');
238+
insertQRCode("");
178239
$('#x2cModal').modal('show');
179240
});
180241
}
181242

243+
function initHelpToggle() {
244+
const helpToggle = document.getElementById('helpToggle');
245+
const welcomeCard = document.getElementById('welcomeCard');
246+
247+
if (helpToggle && welcomeCard) {
248+
helpToggle.addEventListener('click', function(e) {
249+
e.preventDefault();
250+
welcomeCard.classList.toggle('d-none');
251+
});
252+
}
253+
}
254+
182255
$(document).ready(function() {
183256
initSetupCard();
184257
setInterfaceSetupFields();
185258
initQRCodes();
259+
initHelpToggle();
186260

187261
// Toggles for views
188262
const toggleWatch = document.getElementById('toggleWatch');

0 commit comments

Comments
 (0)