Repository: abey79/vpype

Issue:
Close viewer from terminal 
What do you think of the idea of being able to close the viewer form the terminal via ^C ?
My logic is when I activate show - the window pops up automatically, but I am still in terminal. 
To close it I need to move over to the show window and close it. If I could close it via ^C then I could iterate through the code and check any changes very fast as I would remain in terminal.



Discussion:
Good one. Should be easy to do: https://stackoverflow.com/a/4939113/229511

Resolving patch:
diff --git a/vpype_viewer/qtviewer/utils.py b/vpype_viewer/qtviewer/utils.py
--- a/vpype_viewer/qtviewer/utils.py
+++ b/vpype_viewer/qtviewer/utils.py
@@ -1,5 +1,10 @@
 import os
+import signal
+import socket
+from contextlib import contextmanager
+from typing import Callable
 
+from PySide2 import QtNetwork
 from PySide2.QtCore import QCoreApplication
 from PySide2.QtGui import QIcon, QPalette
 from PySide2.QtWidgets import QAction, QActionGroup
@@ -73,3 +78,38 @@ def __init__(self, current: float = 0.8, parent=None):
             act.setCheckable(True)
             act.setChecked(w == current)
             act.setData(w)
+
+
+class SignalWatchdog(QtNetwork.QAbstractSocket):
+    """This object notify PySide2's event loop of an incoming signal and makes it process it.
+
+    The python interpreter flags incoming signals and triggers the handler only upon the next
+    bytecode is processed. Since PySide2's C++ event loop function never/rarely returns when
+    the UX is in the background, the Python interpreter doesn't have a chance to run and call
+    the handler.
+
+    From: https://stackoverflow.com/a/65802260/229511 and
+    https://stackoverflow.com/a/37229299/229511
+    """
+
+    def __init__(self):
+        # noinspection PyTypeChecker
+        super().__init__(QtNetwork.QAbstractSocket.SctpSocket, None)  # type: ignore
+        self.writer, self.reader = socket.socketpair()
+        self.writer.setblocking(False)
+        signal.set_wakeup_fd(self.writer.fileno())
+        self.setSocketDescriptor(self.reader.fileno())
+        self.readyRead.connect(lambda: None)
+
+
+@contextmanager
+def set_sigint_handler(handler: Callable):
+    original_handler = signal.getsignal(signal.SIGINT)
+    signal.signal(signal.SIGINT, handler)
+    # noinspection PyUnusedLocal
+    watchdog = SignalWatchdog()
+    try:
+        yield
+    finally:
+        signal.signal(signal.SIGINT, original_handler)
+        del watchdog
diff --git a/vpype_viewer/qtviewer/viewer.py b/vpype_viewer/qtviewer/viewer.py
--- a/vpype_viewer/qtviewer/viewer.py
+++ b/vpype_viewer/qtviewer/viewer.py
@@ -30,7 +30,7 @@
 
 from .._scales import UnitType
 from ..engine import Engine, ViewMode
-from .utils import PenOpacityActionGroup, PenWidthActionGroup, load_icon
+from .utils import PenOpacityActionGroup, PenWidthActionGroup, load_icon, set_sigint_handler
 
 __all__ = ["QtViewerWidget", "QtViewer", "show"]
 
@@ -480,4 +480,11 @@ def show(
 
     widget.show()
 
-    return app.exec_()
+    # noinspection PyUnusedLocal
+    def sigint_handler(signum, frame):
+        QApplication.quit()
+
+    with set_sigint_handler(sigint_handler):
+        res = app.exec_()
+
+    return res


Test patch: