Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle ctrl-C from terminal and close the viewer #321

Merged
merged 2 commits into from
Oct 18, 2021
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ New features and improvements:

* Relative coordinates are now used by default to reduce file size. If absolute coordinates are needed, they a new `--absolute` option for the `write` command.
* A homing command (as defined by the `final_pu_params` configuration parameter) is no longer emitted between layers.
* The viewer (`show` command) now catches interruptions from the terminal (ctrl-C) and closes itself (#321)

Bug fixes:
* ...
Expand Down
40 changes: 40 additions & 0 deletions vpype_viewer/qtviewer/utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
11 changes: 9 additions & 2 deletions vpype_viewer/qtviewer/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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