wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
Main.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#ifdef Q_OS_LINUX
5#include <cstdio>
6#endif
7
8#ifdef Q_OS_WIN
9#include <windows.h>
10#endif
11
12#include <QCommandLineParser>
13#include <QDir>
14#include <QFile>
15#include <QMessageBox>
16#include <QRegularExpression>
17#include <QStandardPaths>
18
19#ifdef HAVE_SENTRY
20#include <QScopeGuard>
21#include <QSettings>
22#include <QUuid>
23#endif
24
26#include "App/Core/Common.h"
29#include "App/Scene/Workspace.h"
30#include "App/UI/MainWindow.h"
31
32#ifdef ENABLE_MCP_SERVER
34#endif
35
36#ifdef HAVE_SENTRY
37#include "thirdparty/sentry/include/sentry.h"
38#endif
39
40#ifdef Q_OS_LINUX
41namespace {
42
43QtMessageHandler g_previousMessageHandler = nullptr;
44
45void portalQuietMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
46{
47 // Qt 6.10 QDesktopUnixServices issues a Screenshot.version Get before the host-portal
48 // Register, so xdg-desktop-portal caches our sender and rejects Register (QTBUG-145847).
49 if (type == QtWarningMsg && context.category
50 && qstrcmp(context.category, "qt.qpa.services") == 0
51 && message.startsWith(QLatin1String("Failed to register with host portal"))) {
52 return;
53 }
54 // Qt Wayland's text-input v3 backend emits a "leave event for surface 0x0" warning
55 // every time focus changes between widgets — a known Qt issue affecting many apps
56 // (FreeCAD, Transmission, Telegram). No user-actionable signal; spammy on Wayland.
57 if (type == QtWarningMsg && context.category
58 && qstrcmp(context.category, "qt.qpa.wayland.textinput") == 0) {
59 return;
60 }
61 // Qt's HarfBuzz/font database logs "OpenType support missing for <font>, script <N>"
62 // when a fallback font lacks coverage for a particular Unicode script. Harmless —
63 // Qt then falls back to a font that does cover the script. Note: this is emitted
64 // as qDebug, not qCWarning, so we deliberately don't gate on type here.
65 if (message.startsWith(QLatin1String("OpenType support missing"))) {
66 return;
67 }
68 if (g_previousMessageHandler) {
69 g_previousMessageHandler(type, context, message);
70 } else {
71 std::fputs(qPrintable(qFormatLogMessage(type, context, message)), stderr);
72 std::fputc('\n', stderr);
73 }
74}
75
76}
77#endif
78
79int main(int argc, char *argv[])
80{
81#ifdef HAVE_SENTRY
82 // Sentry must be initialised before QApplication so that crashes during Qt
83 // startup are captured; sentry_close() is called via qScopeGuard at process exit
84 // to flush any queued events even if the app exits via an exception or exit().
85 sentry_options_t *options = sentry_options_new();
86 sentry_options_set_dsn(options, "https://719a4881adf6e678b198bf9aad6b4500@o4508704323469312.ingest.us.sentry.io/4508704326352896");
87
88 // Use an absolute path so the crash database is always in the same place
89 // regardless of the working directory the app was launched from.
90 const auto dbPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/sentry-native";
91 sentry_options_set_database_path(options, dbPath.toStdString().c_str());
92
93 sentry_options_set_release(options, "wiredpanda@" APP_VERSION);
94 sentry_options_set_debug(options, 0);
95
96#ifdef QT_DEBUG
97 sentry_options_set_environment(options, "development");
98#else
99 sentry_options_set_environment(options, "production");
100#endif
101
102 sentry_init(options);
103
104 // Anonymous user identification — persisted across sessions via QSettings
105 // so Sentry can count unique affected users and correlate issues.
106 auto userId = QSettings().value("sentry/userId").toString();
107 if (userId.isEmpty()) {
108 userId = QUuid::createUuid().toString(QUuid::WithoutBraces);
109 QSettings().setValue("sentry/userId", userId);
110 }
111 sentry_value_t user = sentry_value_new_object();
112 sentry_value_set_by_key(user, "id", sentry_value_new_string(userId.toStdString().c_str()));
113 sentry_set_user(user);
114
115 // Custom tags for filtering and triage
116 sentry_set_tag("qt.version", QT_VERSION_STR);
117
118 // Make sure everything flushes
119 auto sentryClose = qScopeGuard([] { sentry_close(); });
120#endif
121
122 // Disable all debug/verbose output at startup; the --verbosity option re-enables
123 // it after the command-line parser runs below
125
126#ifdef ENABLE_MCP_SERVER
127 // Early argument parsing for MCP mode detection
128 // This must be done before QApplication creation to set Qt platform correctly
129 // Full argument validation happens later with QCommandLineParser
130 bool mcpMode = false;
131 bool mcpGuiMode = false;
132 for (int i = 1; i < argc; i++) {
133 if (QString(argv[i]) == "--mcp") {
134 mcpMode = true;
135 }
136 if (QString(argv[i]) == "--mcp-gui") {
137 mcpGuiMode = true;
138 }
139 }
140#else
141 bool mcpMode = false;
142 bool mcpGuiMode = false;
143#endif
144
145#ifdef Q_OS_LINUX
146 // On Linux, CLI-only invocations (codegen, waveform export) must not try to
147 // connect to an X11 display — CI runners and headless servers typically don't
148 // have one. Forcing "offscreen" before QApplication avoids a crash.
149 bool isCliMode = false;
150 for (int i = 1; i < argc; ++i) {
151 QString arg = QString::fromLocal8Bit(argv[i]);
152 if (arg == "--arduino-file" || arg == "-a" ||
153 arg == "--waveform" || arg == "-w" ||
154 arg == "--terminal" || arg == "-c" ||
155 arg == "--help" || arg == "-h" ||
156 arg == "--version" || arg == "-v") {
157 isCliMode = true;
158 break;
159 }
160 }
161
162 // Also check if no DISPLAY is available (headless environment)
163 if (isCliMode || !qEnvironmentVariableIsSet("DISPLAY")) {
164 qputenv("QT_QPA_PLATFORM", "offscreen");
165 }
166#endif
167
168#ifdef Q_OS_WIN
169 if (!mcpMode && !mcpGuiMode) {
170 // Only attach console for non-MCP modes to preserve stdin/stdout pipes
171 FILE *fpstdout = stdout, *fpstderr = stderr;
172 if (AttachConsole(ATTACH_PARENT_PROCESS)) {
173 freopen_s(&fpstdout, "CONOUT$", "w", stdout);
174 freopen_s(&fpstderr, "CONOUT$", "w", stderr);
175 }
176 }
177#endif
178
179#ifndef Q_OS_WIN
180 // Set environment for headless operation if in MCP mode without GUI
181 if (mcpMode && !mcpGuiMode) {
182 qputenv("QT_QPA_PLATFORM", "offscreen");
183 }
184#endif
185
186#ifdef Q_OS_LINUX
187 g_previousMessageHandler = qInstallMessageHandler(portalQuietMessageHandler);
188
189 // When launched as an AppImage the runtime changes CWD to the squashfs mount
190 // point. OWD holds the directory the user was actually in, so relative file
191 // paths from CLI args and file dialogs resolve correctly.
192 if (qEnvironmentVariableIsSet("APPIMAGE")) {
193 QDir::setCurrent(qEnvironmentVariable("OWD"));
194 }
195#endif
196
197 Application app(argc, argv);
198 app.setOrganizationName("GIBIS-UNIFESP");
199 app.setApplicationName("wiRedPanda");
200 app.setApplicationVersion(APP_VERSION);
201 app.setDesktopFileName("wiredpanda");
202 // Fusion style provides a consistent cross-platform look and is required for
203 // the custom theme colours defined in ThemeManager to render correctly on all OSes
204 app.setStyle("Fusion");
205 // Build the window icon from pre-rasterised PNGs so it is always available
206 // on the taskbar (SVG rendering requires a plugin that may not be loaded yet
207 // at this early startup point, causing a null icon on KDE Wayland / Steam Deck).
208 QIcon appIcon;
209 appIcon.addPixmap(QPixmap(":/Assets/Icons/64x64/wpanda.png"));
210 appIcon.addPixmap(QPixmap(":/Assets/Icons/128x128/wpanda.png"));
211 app.setWindowIcon(appIcon);
212
213 // Self-registering element metadata (static initialisers in each element's
214 // .cpp) may trigger ThemeManager construction before QApplication exists.
215 // When that happens the dark palette is silently dropped because
216 // Application::instance() is still null. Re-apply the persisted theme now
217 // that the Application and Fusion style are ready.
219
220#ifdef Q_OS_LINUX
221 // When running as an AppImage, the Wayland compositor resolves the window icon
222 // by looking up the desktop file for the app_id ("wiredpanda") in the system's
223 // XDG_DATA_DIRS — it does not see the AppImage's internal share directory.
224 // Self-registering the desktop file and icon to ~/.local/share/ makes the icon
225 // visible to the compositor without requiring appimaged or manual installation.
226 {
227 const QString appImagePath = qEnvironmentVariable("APPIMAGE");
228 const QString appDir = qEnvironmentVariable("APPDIR");
229 const QString localShare = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
230 const QString appsDir = localShare + "/applications";
231 const QString desktopDst = appsDir + "/wiredpanda.desktop";
232
233 if (!appImagePath.isEmpty() && !appDir.isEmpty()) {
234 // Build the desired desktop file content: take the bundled template and
235 // patch in the current AppImage path. Comparing this against whatever is
236 // installed detects both a changed path and any stale content from an older
237 // release in one step.
238 QString desiredDesktop;
239 {
240 QFile src(appDir + "/usr/share/applications/wiredpanda.desktop");
241 if (src.open(QIODevice::ReadOnly)) {
242 desiredDesktop = QString::fromUtf8(src.readAll());
243 desiredDesktop.replace(QRegularExpression("^Exec=.*$", QRegularExpression::MultilineOption),
244 "Exec=\"" + appImagePath + "\" %F");
245 }
246 }
247
248 if (!desiredDesktop.isEmpty()) {
249 bool needsUpdate = true;
250 {
251 QFile installed(desktopDst);
252 if (installed.open(QIODevice::ReadOnly))
253 needsUpdate = (QString::fromUtf8(installed.readAll()) != desiredDesktop);
254 }
255 if (needsUpdate) {
256 QDir().mkpath(appsDir);
257 QFile dst(desktopDst);
258 if (dst.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
259 const QByteArray bytes = desiredDesktop.toUtf8();
260 if (dst.write(bytes) != bytes.size()) {
261 qCWarning(zero) << "Failed to write desktop entry:" << dst.errorString();
262 }
263 }
264 }
265 }
266
267 // Install icons whenever they are absent — independently of whether the
268 // desktop entry needed updating. Previous versions only installed the SVG,
269 // so users running the same AppImage path would never get the PNG installed
270 // because needsUpdate was false on every subsequent launch.
271 const QString iconsDir = localShare + "/icons/hicolor/scalable/apps";
272 QDir().mkpath(iconsDir);
273 const QString iconDst = iconsDir + "/wpanda.svg";
274 if (!QFile::exists(iconDst))
275 QFile::copy(appDir + "/usr/share/icons/hicolor/scalable/apps/wpanda.svg", iconDst);
276
277 const QString icons256Dir = localShare + "/icons/hicolor/256x256/apps";
278 QDir().mkpath(icons256Dir);
279 const QString icon256Dst = icons256Dir + "/wpanda.png";
280 if (!QFile::exists(icon256Dst))
281 QFile::copy(appDir + "/usr/share/icons/hicolor/256x256/apps/wpanda.png", icon256Dst);
282 } else if (QFile::exists(desktopDst)) {
283 // Not running as AppImage — remove stale self-registered desktop entry
284 // left by a previous AppImage run, so it doesn't shadow the system-wide
285 // entry installed by cmake --install.
286 QFile::remove(desktopDst);
287 }
288 }
289#endif
290
291 try {
292 QCommandLineParser parser;
293 parser.setApplicationDescription(app.applicationName());
294 parser.addHelpOption();
295 parser.addVersionOption();
296 parser.addPositionalArgument("file", QCoreApplication::translate("main", "Circuit file to open."));
297
298 QCommandLineOption verbosityOption(
299 {"v2", "verbosity"},
300 QCoreApplication::translate("main", "Verbosity level 0 to 5, disabled by default."),
301 QCoreApplication::translate("main", "verbosity level"));
302 parser.addOption(verbosityOption);
303
304 QCommandLineOption arduinoFileOption(
305 {"a", "arduino-file"},
306 QCoreApplication::translate("main", "Exports circuit to <arduino-file>."),
307 QCoreApplication::translate("main", "arduino file"));
308 parser.addOption(arduinoFileOption);
309
310 QCommandLineOption waveformFileOption(
311 {"w", "waveform"},
312 QCoreApplication::translate("main", "Exports circuit to waveform text file."),
313 QCoreApplication::translate("main", "waveform input text file"));
314 parser.addOption(waveformFileOption);
315
316 QCommandLineOption terminalFileOption(
317 {"c", "terminal"},
318 QCoreApplication::translate("main", "Exports circuit to waveform text file, reading input from terminal."));
319 parser.addOption(terminalFileOption);
320
321 QCommandLineOption blockTruthTableOption(
322 {"btt", "blockTruthTable"},
323 QCoreApplication::translate("main", "When used with -c/--terminal, block execution if the circuit contains Truth Tables."));
324 parser.addOption(blockTruthTableOption);
325
326#ifdef ENABLE_MCP_SERVER
327 QCommandLineOption mcpModeOption(
328 "mcp",
329 QCoreApplication::translate("main", "Run in MCP (Model Context Protocol) mode for programmatic control."));
330 parser.addOption(mcpModeOption);
331
332 QCommandLineOption mcpGuiOption(
333 "mcp-gui",
334 QCoreApplication::translate("main", "Run MCP mode with a visible GUI window."));
335 parser.addOption(mcpGuiOption);
336#endif
337
338 parser.process(app);
339
340 if (const QString verbosity = parser.value(verbosityOption); !verbosity.isEmpty()) {
341 Comment::setVerbosity(verbosity.toInt());
342 }
343
344 QStringList args = parser.positionalArguments();
345
346 // --- Non-interactive batch operations ---
347 // Each branch loads the circuit, runs the export, then exits immediately
348 // without entering the Qt event loop. Application::interactiveMode is set
349 // to false so that error dialogs are suppressed and errors go to stderr only.
350
351 // Resolve the positional argument against the original working
352 // directory. QFile would handle a relative path on its own, but
353 // resolving up-front means any "file does not exist" error reports the
354 // absolute path the user actually opened, which is far less confusing
355 // than echoing back a bare basename.
356 const QString inputFile = args.empty() ? QString() : QDir::current().absoluteFilePath(args.at(0));
357
358 if (const QString arduFile = parser.value(arduinoFileOption); !arduFile.isEmpty()) {
359 if (inputFile.isEmpty()) {
360 QTextStream(stderr) << QCoreApplication::translate("main", "Error: no input circuit file given.") << '\n';
361 return 1;
362 }
364 MainWindow window;
365 window.loadPandaFile(inputFile);
366 window.exportToArduino(arduFile);
367 return 0;
368 }
369
370 if (const QString wfFile = parser.value(waveformFileOption); !wfFile.isEmpty()) {
371 if (inputFile.isEmpty()) {
372 QTextStream(stderr) << QCoreApplication::translate("main", "Error: no input circuit file given.") << '\n';
373 return 1;
374 }
376 MainWindow window;
377 window.loadPandaFile(inputFile);
378 window.exportToWaveFormFile(wfFile);
379 return 0;
380 }
381
382 if (const bool isTerminal = parser.isSet(terminalFileOption); isTerminal) {
383 if (inputFile.isEmpty()) {
384 QTextStream(stderr) << QCoreApplication::translate("main", "Error: no input circuit file given.") << '\n';
385 return 1;
386 }
387 {
389 MainWindow window;
390 window.loadPandaFile(inputFile);
391
392 // --blockTruthTable allows callers (e.g. automated graders) to
393 // reject circuits that contain truth tables, which cannot be
394 // reliably evaluated via the terminal waveform interface.
395 if (parser.isSet(blockTruthTableOption)) {
396 bool containsTruthTable = false;
397 auto elements = window.currentTab()->scene()->elements();
398
399 for (const auto *element : elements) {
400 if (element->elementType() == ElementType::TruthTable) {
401 containsTruthTable = true;
402 break;
403 }
404 }
405
406 if (containsTruthTable) {
407 QTextStream(stderr) << QCoreApplication::translate("main", "Error: Circuit contains Truth Table elements.") << '\n';
408 return 1;
409 }
410 }
411
413 }
414 return 0;
415 }
416
417#ifdef ENABLE_MCP_SERVER
418 // Handle MCP mode
419 if (parser.isSet(mcpModeOption) || parser.isSet(mcpGuiOption)) {
421
422 auto *window = new MainWindow();
423
424 // Show window only if GUI mode is requested
425 if (parser.isSet(mcpGuiOption)) {
426 window->show();
427 }
428
429 // Start MCP processor
430 MCPProcessor processor(window);
431 processor.startProcessing();
432
433 return app.exec();
434 }
435#endif
436
437 auto *window = new MainWindow();
438 window->show();
439
440 if (!inputFile.isEmpty()) {
441 window->loadPandaFile(inputFile);
442 }
443 } catch (const std::exception &e) {
445 QMessageBox::critical(nullptr, QObject::tr("Error!"), e.what());
446 } else {
447 // Non-interactive (CLI / MCP) modes have no dialog to fall back on;
448 // without this, the process exits silently and the user sees no
449 // indication that the file failed to load.
450 QTextStream(stderr) << QCoreApplication::translate("main", "Error: ") << e.what() << '\n';
451 }
452 return 1;
453 }
454
455 return app.exec();
456}
Custom QApplication subclass with exception handling and main-window access.
Common logging utilities, the Pandaception error type, and helper macros.
Abstract base class for all graphical circuit elements.
Main application window providing menus, toolbars, and tab management.
int main(int argc, char *argv[])
Definition Main.cpp:79
Theme management types and singleton ThemeManager.
WorkSpace widget: the complete circuit editing environment for one tab.
Custom QApplication that wraps event dispatch with exception handling.
Definition Application.h:45
static bool interactiveMode
Definition Application.h:73
static void setVerbosity(const int verbosity)
Sets the active logging verbosity level.
Definition Common.cpp:13
Model Context Protocol processor for wiRedPanda.
void startProcessing()
The top-level application window hosting the tab bar, menus, element palette, and editor.
Definition MainWindow.h:47
void loadPandaFile(const QString &fileName)
Loads a .panda circuit file into the current or a new tab.
void exportToArduino(QString fileName)
Generates Arduino sketch code for the current circuit.
WorkSpace * currentTab() const override
Returns the currently visible WorkSpace tab, or nullptr.
void exportToWaveFormTerminal()
Exports the current waveform simulation to standard output (terminal).
void exportToWaveFormFile(const QString &fileName)
Saves the BeWavedDolphin waveform session to fileName.
const QVector< GraphicElement * > elements() const
Returns all graphic elements in the scene.
Definition Scene.cpp:336
static Theme theme()
Returns the currently active theme.
static void setTheme(const Theme theme)
Switches the application to theme and emits themeChanged().
Scene * scene()
Returns the Scene embedded in this workspace.