gui: robust window activation + headless --selftest/--version flags

Center the main window on the active screen and raise()/activateWindow()
it on launch, so it cannot open off-screen or behind the focused frame on
a tiling compositor (Sway/i3/Hyprland) — the usual cause of reports that
'the GUI won't start / is stuck': it launched, but was not visible.

Add lightweight non-GUI flags handled before the QApplication is built:
  --version  print GUI + pinned-CLI version and Qt binding
  --help     usage
  --selftest build the full UI, spin the event loop once, exit 0
so a successful launch can be verified with no display (headless/CI/remote).
Also emit a one-line 'window open' notice to stderr so a terminal launch
is not mistaken for a hung shell.
This commit is contained in:
Cristian Cezar Moisés 2026-07-10 23:54:32 -03:00
commit e15329a7c9

View file

@ -23,7 +23,7 @@ try:
QTextEdit, QProgressBar, QTabWidget, QFrame, QCheckBox, QTextEdit, QProgressBar, QTabWidget, QFrame, QCheckBox,
QSpinBox, QMessageBox, QStatusBar, QScrollArea QSpinBox, QMessageBox, QStatusBar, QScrollArea
) )
from PySide6.QtCore import Qt, Signal, QObject, QThread from PySide6.QtCore import Qt, Signal, QObject, QThread, QTimer
from PySide6.QtGui import QPalette, QColor, QIcon, QPixmap from PySide6.QtGui import QPalette, QColor, QIcon, QPixmap
QT_BINDING = "PySide6" QT_BINDING = "PySide6"
except ImportError: except ImportError:
@ -34,7 +34,7 @@ except ImportError:
QTextEdit, QProgressBar, QTabWidget, QFrame, QCheckBox, QTextEdit, QProgressBar, QTabWidget, QFrame, QCheckBox,
QSpinBox, QMessageBox, QStatusBar, QScrollArea QSpinBox, QMessageBox, QStatusBar, QScrollArea
) )
from PyQt6.QtCore import Qt, pyqtSignal as Signal, QObject, QThread from PyQt6.QtCore import Qt, pyqtSignal as Signal, QObject, QThread, QTimer
from PyQt6.QtGui import QPalette, QColor, QIcon, QPixmap from PyQt6.QtGui import QPalette, QColor, QIcon, QPixmap
QT_BINDING = "PyQt6" QT_BINDING = "PyQt6"
except ImportError: except ImportError:
@ -858,9 +858,26 @@ class ZuptWindow(QMainWindow):
def main(): def main():
compress_files = extract_file = None
args = sys.argv[1:] args = sys.argv[1:]
if args:
# Lightweight non-GUI flags first, so `vaptvupt-gui --version|--help|--selftest`
# work with no display and aren't mistaken for files to compress. `--selftest`
# is a headless-friendly smoke test: it builds the whole UI and spins the event
# loop once, then exits 0 — the reliable way to confirm the GUI stack launches
# on a machine where the window itself is hard to see (tiling WM, remote, CI).
if args and args[0] in ("--version", "-V", "version"):
print(f"vaptvupt-gui {ZUPT_VER_NUMBER} ({QT_BINDING}) | CLI: {VAPTVUPT}")
return 0
if args and args[0] in ("--help", "-h", "help"):
print("usage: vaptvupt-gui [ARCHIVE.zupt | --extract ARCHIVE.zupt |\n"
" --compress FILE [FILE ...]]\n"
" vaptvupt-gui --selftest # verify the GUI launches (no window kept)\n"
" vaptvupt-gui --version")
return 0
compress_files = extract_file = None
selftest = ("--selftest" in args[:1])
if args and not selftest:
if args[0] == "--compress" and len(args) > 1: compress_files = args[1:] if args[0] == "--compress" and len(args) > 1: compress_files = args[1:]
elif args[0] == "--extract" and len(args) > 1: extract_file = args[1] elif args[0] == "--extract" and len(args) > 1: extract_file = args[1]
elif args[0].endswith(".zupt"): extract_file = args[0] elif args[0].endswith(".zupt"): extract_file = args[0]
@ -882,8 +899,34 @@ def main():
pal.setColor(role, QColor(c)) pal.setColor(role, QColor(c))
app.setPalette(pal) app.setPalette(pal)
win = ZuptWindow(compress_files=compress_files, extract_file=extract_file) win = ZuptWindow(compress_files=compress_files, extract_file=extract_file)
if selftest:
win.show()
QTimer.singleShot(400, app.quit)
rc = app.exec()
print(f"selftest OK — {QT_BINDING}: window + {win.tabs.count()} tabs built, "
f"event loop ran (rc={rc}); CLI={VAPTVUPT}")
return rc
# Center on the active screen and force focus. Without this the window can
# open off-screen or behind the last-focused frame on a tiling compositor
# (Sway/i3/Hyprland) — the usual cause of "the GUI won't start / is stuck":
# it launched, but you can't see it.
scr = app.primaryScreen()
if scr is not None:
fg = win.frameGeometry()
fg.moveCenter(scr.availableGeometry().center())
win.move(fg.topLeft())
win.show() win.show()
sys.exit(app.exec()) win.raise_()
win.activateWindow()
# A GUI blocks the launching shell, so a working launch otherwise looks like
# a "stuck" terminal. Emit one line to stderr so it's unambiguous.
sys.stderr.write(f"VaptVupt {ZUPT_VER_NUMBER} GUI started — window open "
f"(close it to exit).\n")
sys.stderr.flush()
return app.exec()
if __name__ == "__main__": if __name__ == "__main__":
main() sys.exit(main())