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
7 changes: 3 additions & 4 deletions src/frontEnd/TerminalUi.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def cancelSimulation(self):
self.cancelSimulationButton.setEnabled(False)
self.redoSimulationButton.setEnabled(True)

if (self.qProcess.state() == QtCore.QProcess.NotRunning):
if (self.qProcess.state() == QtCore.QProcess.ProcessState.NotRunning):
return

self.simulationCancelled = True
Expand All @@ -98,7 +98,7 @@ def redoSimulation(self):
self.cancelSimulationButton.setEnabled(True)
self.redoSimulationButton.setEnabled(False)

if (self.qProcess.state() != QtCore.QProcess.NotRunning):
if (self.qProcess.state() != QtCore.QProcess.ProcessState.NotRunning):
return

# To make the progressbar running
Expand All @@ -122,8 +122,7 @@ def redoSimulation(self):
else:
self.Flag = False

# Emit a custom signal with name plotFlag2 depending upon the Flag
self.qProcess.setProperty("plotFlag2", self.Flag)
self.qProcess.setProperty("redoPlotFlag", self.Flag)

self.qProcess.start('ngspice', self.args)

Expand Down
169 changes: 70 additions & 99 deletions src/ngspiceSimulation/NgspiceWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import os
import shlex
import logging
from typing import List, Optional
from PyQt6 import QtWidgets, QtCore
Expand Down Expand Up @@ -56,7 +57,6 @@ def __init__(self, netlist: str, sim_end_signal: pyqtSignal, plotFlag: Optional[
"""
super().__init__()

# **CRITICAL FIX**: Set expanding size policy
self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Expanding)

Expand All @@ -68,7 +68,6 @@ def __init__(self, netlist: str, sim_end_signal: pyqtSignal, plotFlag: Optional[
self.netlist_path = netlist
self.sim_end_signal = sim_end_signal

# **IMPORTANT**: Store plotFlag and command for dual plot functionality
self.plotFlag = plotFlag
self.command = netlist
logger.info(f"Value of plotFlag: {self.plotFlag}")
Expand Down Expand Up @@ -109,10 +108,11 @@ def _prepare_ngspice_arguments(self, netlist: str) -> List[str]:
def _configure_process(self) -> None:
"""Configure the NGSpice process with working directory and signals."""
self.process.setWorkingDirectory(self.project_dir)
self.process.setProcessChannelMode(QtCore.QProcess.ProcessChannelMode.MergedChannels)
self.process.setProcessChannelMode(QtCore.QProcess.ProcessChannelMode.SeparateChannels)

# Connect process signals
self.process.readyRead.connect(self.ready_read_all)
self.process.readyReadStandardOutput.connect(self._handle_stdout)
self.process.readyReadStandardError.connect(self._handle_stderr)
self.process.finished.connect(
lambda exit_code, exit_status: self.finish_simulation(
exit_code, exit_status, self.sim_end_signal, False
Expand All @@ -122,19 +122,18 @@ def _configure_process(self) -> None:
lambda: self.finish_simulation(None, None, self.sim_end_signal, True)
)

def _register_process(self, process: QtCore.QProcess) -> None:
"""Register a process with the application config tracker."""
self.obj_appconfig.process_obj.append(process)
current_project_name = self.obj_appconfig.current_project['ProjectName']
if current_project_name in self.obj_appconfig.proc_dict:
self.obj_appconfig.proc_dict[current_project_name].append(process.pid())

def _start_process(self) -> None:
"""Start the NGSpice process and register it with the application."""
self.process.start('ngspice', self.ngspice_args)

# Register process with application config
self.obj_appconfig.process_obj.append(self.process)
logger.debug(f"Process dictionary: {self.obj_appconfig.proc_dict}")

current_project_name = self.obj_appconfig.current_project['ProjectName']
if current_project_name in self.obj_appconfig.proc_dict:
self.obj_appconfig.proc_dict[current_project_name].append(
self.process.pid()
)
self._register_process(self.process)

def _is_linux(self) -> bool:
"""Check if the current operating system is Linux."""
Expand All @@ -150,46 +149,40 @@ def _start_gaw_process(self, netlist: str) -> None:
try:
self.gaw_process = QtCore.QProcess(self)
raw_file = netlist.replace(".cir.out", ".raw")
self.gaw_command = f"gaw {raw_file}"
self.gaw_command = f"gaw {shlex.quote(raw_file)}"
self.gaw_process.start('sh', ['-c', self.gaw_command])
logger.info(f"Started GAW with command: {self.gaw_command}")
except Exception as e:
logger.error(f"Failed to start GAW process: {e}")

@pyqtSlot()
def ready_read_all(self) -> None:
"""
Handle process output and display it in the terminal console.

Reads both standard output and standard error from the NGSpice process
and displays them in the TerminalUi console. Filters out specific
NGSpice warnings that are not relevant in batch mode.
"""
def _handle_stdout(self) -> None:
"""Read stdout from the NGSpice process and display in terminal."""
try:
data = self.process.readAllStandardOutput().data()
if data:
self.terminal_ui.simulationConsole.insertPlainText(
data.decode('utf-8', errors='replace')
)
except Exception as e:
logger.error(f"Error reading stdout: {e}")

@pyqtSlot()
def _handle_stderr(self) -> None:
"""Read stderr, filter batch-mode noise, display the rest."""
try:
# Read and display standard output
std_output = self.process.readAllStandardOutput().data()
if std_output:
output_text = str(std_output, encoding='utf-8')
self.terminal_ui.simulationConsole.insertPlainText(output_text)

# Read and filter standard error
std_error = self.process.readAllStandardError().data()
if std_error:
error_text = str(std_error, encoding='utf-8')

# Filter out irrelevant NGSpice warnings in batch mode
filtered_lines = []
for line in error_text.split('\n'):
if ('PrinterOnly' not in line and
'viewport for graphics' not in line):
filtered_lines.append(line)

filtered_error = '\n'.join(filtered_lines)
if filtered_error.strip():
self.terminal_ui.simulationConsole.insertPlainText(filtered_error)

data = self.process.readAllStandardError().data()
if not data:
return
text = data.decode('utf-8', errors='replace')
filtered = '\n'.join(
line for line in text.split('\n')
if 'PrinterOnly' not in line and 'viewport for graphics' not in line
)
if filtered.strip():
self.terminal_ui.simulationConsole.insertPlainText(filtered)
except Exception as e:
logger.error(f"Error reading process output: {e}")
logger.error(f"Error reading stderr: {e}")

def finish_simulation(self, exit_code: Optional[int],
exit_status: Optional[QtCore.QProcess.ExitStatus],
Expand Down Expand Up @@ -220,7 +213,9 @@ def finish_simulation(self, exit_code: Optional[int],
exit_code = self.process.exitCode()

error_type = self.process.error()
if error_type <= self.ERROR_TIMED_OUT: # FailedToStart, Crashed, TimedOut
if error_type in (QtCore.QProcess.ProcessError.FailedToStart,
QtCore.QProcess.ProcessError.Crashed,
QtCore.QProcess.ProcessError.Timedout):
exit_status = QtCore.QProcess.ExitStatus.CrashExit
elif exit_status is None:
exit_status = self.process.exitStatus()
Expand All @@ -230,18 +225,13 @@ def finish_simulation(self, exit_code: Optional[int],
self._show_cancellation_message()
elif self._is_simulation_successful(exit_status, exit_code, error_type):
self._show_success_message()

# **CRITICAL ADDITION**: Check and update plotFlag from process properties
# This handles the re-simulation case from TerminalUi
new_plot_flag = self.process.property("plotFlag")
if new_plot_flag is not None:
self.plotFlag = new_plot_flag

new_plot_flag2 = self.process.property("plotFlag2")
if new_plot_flag2 is not None:
self.plotFlag = new_plot_flag2

# **CRITICAL ADDITION**: Open NGSpice plot windows if requested

# On redo-simulation, TerminalUi sets "redoPlotFlag" on the process
# to pass the user's plot choice back here
redo_flag = self.process.property("redoPlotFlag")
if redo_flag is not None:
self.plotFlag = redo_flag

if self.plotFlag:
self.open_ngspice_plots()
else:
Expand All @@ -266,55 +256,36 @@ def open_ngspice_plots(self) -> None:
config_path = os.path.join('library', 'config', '.nghdl', 'config.ini')
parser_nghdl.read(config_path)
msys_home = parser_nghdl.get('COMPILER', 'MSYS_HOME')

temp_dir = os.getcwd()
os.chdir(self.project_dir)

# Create command for Windows using mintty
mintty_command = (
f'cmd /c "start /min {msys_home}/usr/bin/mintty.exe '
f'ngspice -p {self.command}"'
)

# Create a new QProcess for mintty
mintty_exe = os.path.join(msys_home, 'usr', 'bin', 'mintty.exe')

self.mintty_process = QtCore.QProcess(self)
self.mintty_process.start(mintty_command)

os.chdir(temp_dir)
logger.info(f"Started mintty with command: {mintty_command}")
self.mintty_process.setWorkingDirectory(self.project_dir)
# Pass program + args directly — Qt handles quoting internally
self.mintty_process.start(mintty_exe, ['ngspice', '-p', self.command])
logger.info(f"Started mintty: {mintty_exe} ngspice -p {self.command}")

except Exception as e:
logger.error(f"Failed to start Windows NGSpice plots: {e}")

else: # Linux/Unix
try:
# Create xterm command for interactive NGSpice
raw_file = self.command.replace('.cir.out', '.raw')
# Quote all paths so spaces in project names don't break the shell command
xterm_command = (
f"cd {self.project_dir}; "
f"ngspice -r {self.command.replace('.cir.out', '.raw')} "
f"{self.command}"
f"cd {shlex.quote(self.project_dir)} && "
f"ngspice -r {shlex.quote(raw_file)} {shlex.quote(self.command)}"
)
xterm_args = ['-hold', '-e', xterm_command]

# Create new QProcess for xterm
self.xterm_process = QtCore.QProcess(self)
self.xterm_process.start('xterm', xterm_args)

# Register the process
self.obj_appconfig.process_obj.append(self.xterm_process)
current_project = self.obj_appconfig.current_project['ProjectName']
if current_project in self.obj_appconfig.proc_dict:
self.obj_appconfig.proc_dict[current_project].append(
self.xterm_process.pid()
)

# Also restart GAW for the new plot window
self.xterm_process.start('xterm', ['-hold', '-e', xterm_command])

self._register_process(self.xterm_process)

if hasattr(self, 'gaw_process') and hasattr(self, 'gaw_command'):
self.gaw_process.start('sh', ['-c', self.gaw_command])
logger.info(f"Restarted GAW: {self.gaw_command}")
logger.info(f"Started xterm with args: {xterm_args}")

logger.info(f"Started xterm: {xterm_command}")

except Exception as e:
logger.error(f"Failed to start Linux NGSpice plots: {e}")

Expand All @@ -339,8 +310,8 @@ def _is_simulation_successful(self, exit_status: QtCore.QProcess.ExitStatus,
Returns:
True if simulation was successful, False otherwise
"""
return (exit_status == QtCore.QProcess.ExitStatus.NormalExit and
exit_code == 0 and
return (exit_status == QtCore.QProcess.ExitStatus.NormalExit and
exit_code == 0 and
error_type == QtCore.QProcess.ProcessError.UnknownError)

def _show_cancellation_message(self) -> None:
Expand Down
Loading