We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
CTRL-C should exit napari gracefully
## 🐛 Bug

After #1476 napari just prints `KeyboardInterrupt` when CTRL-C is pressed in the system terminal window that used to launch napari.  Prior to 1476 it exited with a crash, which got the job done but was not great. 

Ideally napari would exit gracefully when you hit CTRL-C in the system terminal window.


## To Reproduce

Steps to reproduce the behavior:

1. From a system terminal (e.g. Terminal program on mac)
2. Run "napari" or a script that uses `napari.gui_qt()`
3. Switch back to the terminal window and type CTRL-C

## Expected behavior

Napari exits gracefully.

## Environment

```
napari: not-installed
Platform: macOS-10.15.3-x86_64-i386-64bit
Python: 3.8.1 (default, Jan 8 2020, 16:15:59) [Clang 4.0.1 (tags/RELEASE_401/final)]
Qt: 5.14.2
PyQt5: 5.14.2
NumPy: 1.18.4
SciPy: 1.4.1
Dask: 2.17.2
VisPy: 0.6.5.dev111+g8387ea1a.d20200424

GL version: 2.1 ATI-3.5.5
MAX_TEXTURE_SIZE: 16384

Plugins:
- napari-plugin-engine: 0.1.6
- svg: 0.1.3
```

## Additional context

This is low priority since you can exit with the Quit command, or from the system terminal hit CTRL-Z and `kill %1` the app if necessary. However it seems like exiting gracefully is the right behavior long term.

I tried adding this to our new `ExceptionHandler` class:
```
        # Interpret CTRL-C as a request to quit.
        if isinstance(value, KeyboardInterrupt):
            QApplication.instance().quit()
            return
```
but while it exits cleanly sometimes, sometimes it bus errors or seg faults.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `napari/_qt/exceptions.py`
Content:
```
1 import logging
2 import os
3 import traceback
4 from types import TracebackType
5 from typing import Optional, Type
6 
7 from qtpy.QtCore import QObject, Signal
8 
9 from .qt_error_notification import NapariNotification
10 
11 
12 class ExceptionHandler(QObject):
13     """General class to handle all uncaught exceptions in the Qt event loop.
14 
15     Parameters
16     ----------
17     parent : QObject, optional
18         parent object, by default None
19     gui_exceptions : bool, optional
20         Whether to show exceptions as, by default True.  May be overriden by
21         environment variable: ``NAPARI_CATCH_ERRORS=1`
22         Note: this ``False`` by default in ``gui_qt()`` (the main
23         instantiator of this class), but it is ``True`` in ``napari.__main__``.
24         As a result, exceptions will be shown in the GUI only (mostly) when
25         running napari as ``napari`` or ``python -m napari`` from the command
26         line.
27     """
28 
29     error = Signal(tuple)
30     message: Optional[NapariNotification] = None
31 
32     def __init__(self, parent=None, *, gui_exceptions=True):
33         super().__init__(parent)
34         if os.getenv("NAPARI_CATCH_ERRORS") in ('0', 'False'):
35             self.gui_exceptions = False
36         else:
37             self.gui_exceptions = gui_exceptions
38 
39     def handle(
40         self,
41         etype: Type[BaseException],
42         value: BaseException,
43         tb: TracebackType,
44     ):
45         """Our sys.excepthook override.
46 
47         This function handles uncaught exceptions and can delegate to a
48         secondary handler, whether it be a GUI dialog, or an IPython traceback
49         printout.  The override to ``sys.excepthook`` happens in
50         :func:`napari.gui_qt`, and therefore this is only active when the qt
51         event loop has been started by napari.
52 
53         The three parameters here are what would be returned from
54         :func:`sys.exc_info()`.
55 
56         Parameters
57         ----------
58         etype : Type[BaseException]
59             The type of error raised
60         value : BaseException
61             The error instance
62         tb : TracebackType
63             The traceback object associated with the error.
64         """
65         if self.gui_exceptions:
66             self._show_error_dialog(value)
67         else:
68             text = "".join(traceback.format_exception(etype, value, tb))
69             logging.error("Unhandled exception:\n%s", text)
70         self.error.emit((etype, value, tb))
71 
72     def _show_error_dialog(self, exception: BaseException):
73         self.message = NapariNotification.from_exception(exception)
74         self.message.show()
75
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/napari/_qt/exceptions.py b/napari/_qt/exceptions.py
--- a/napari/_qt/exceptions.py
+++ b/napari/_qt/exceptions.py
@@ -1,5 +1,6 @@
 import logging
 import os
+import sys
 import traceback
 from types import TracebackType
 from typing import Optional, Type
@@ -62,6 +63,12 @@
         tb : TracebackType
             The traceback object associated with the error.
         """
+        # etype.__module__ contains the module raising the error
+        # Custom exception classes can have different behavior
+        # can add custom exception handlers here ...
+        if isinstance(value, KeyboardInterrupt):
+            print("Closed by KeyboardInterrupt", file=sys.stderr)
+            sys.exit(1)
         if self.gui_exceptions:
             self._show_error_dialog(value)
         else:

{"golden_diff": "diff --git a/napari/_qt/exceptions.py b/napari/_qt/exceptions.py\n--- a/napari/_qt/exceptions.py\n+++ b/napari/_qt/exceptions.py\n@@ -1,5 +1,6 @@\n import logging\n import os\n+import sys\n import traceback\n from types import TracebackType\n from typing import Optional, Type\n@@ -62,6 +63,12 @@\n         tb : TracebackType\n             The traceback object associated with the error.\n         \"\"\"\n+        # etype.__module__ contains the module raising the error\n+        # Custom exception classes can have different behavior\n+        # can add custom exception handlers here ...\n+        if isinstance(value, KeyboardInterrupt):\n+            print(\"Closed by KeyboardInterrupt\", file=sys.stderr)\n+            sys.exit(1)\n         if self.gui_exceptions:\n             self._show_error_dialog(value)\n         else:\n", "issue": "CTRL-C should exit napari gracefully\n## \ud83d\udc1b Bug\r\n\r\nAfter #1476 napari just prints `KeyboardInterrupt` when CTRL-C is pressed in the system terminal window that used to launch napari.  Prior to 1476 it exited with a crash, which got the job done but was not great. \r\n\r\nIdeally napari would exit gracefully when you hit CTRL-C in the system terminal window.\r\n\r\n\r\n## To Reproduce\r\n\r\nSteps to reproduce the behavior:\r\n\r\n1. From a system terminal (e.g. Terminal program on mac)\r\n2. Run \"napari\" or a script that uses `napari.gui_qt()`\r\n3. Switch back to the terminal window and type CTRL-C\r\n\r\n## Expected behavior\r\n\r\nNapari exits gracefully.\r\n\r\n## Environment\r\n\r\n```\r\nnapari: not-installed\r\nPlatform: macOS-10.15.3-x86_64-i386-64bit\r\nPython: 3.8.1 (default, Jan 8 2020, 16:15:59) [Clang 4.0.1 (tags/RELEASE_401/final)]\r\nQt: 5.14.2\r\nPyQt5: 5.14.2\r\nNumPy: 1.18.4\r\nSciPy: 1.4.1\r\nDask: 2.17.2\r\nVisPy: 0.6.5.dev111+g8387ea1a.d20200424\r\n\r\nGL version: 2.1 ATI-3.5.5\r\nMAX_TEXTURE_SIZE: 16384\r\n\r\nPlugins:\r\n- napari-plugin-engine: 0.1.6\r\n- svg: 0.1.3\r\n```\r\n\r\n## Additional context\r\n\r\nThis is low priority since you can exit with the Quit command, or from the system terminal hit CTRL-Z and `kill %1` the app if necessary. However it seems like exiting gracefully is the right behavior long term.\r\n\r\nI tried adding this to our new `ExceptionHandler` class:\r\n```\r\n        # Interpret CTRL-C as a request to quit.\r\n        if isinstance(value, KeyboardInterrupt):\r\n            QApplication.instance().quit()\r\n            return\r\n```\r\nbut while it exits cleanly sometimes, sometimes it bus errors or seg faults.\n", "before_files": [{"content": "import logging\nimport os\nimport traceback\nfrom types import TracebackType\nfrom typing import Optional, Type\n\nfrom qtpy.QtCore import QObject, Signal\n\nfrom .qt_error_notification import NapariNotification\n\n\nclass ExceptionHandler(QObject):\n    \"\"\"General class to handle all uncaught exceptions in the Qt event loop.\n\n    Parameters\n    ----------\n    parent : QObject, optional\n        parent object, by default None\n    gui_exceptions : bool, optional\n        Whether to show exceptions as, by default True.  May be overriden by\n        environment variable: ``NAPARI_CATCH_ERRORS=1`\n        Note: this ``False`` by default in ``gui_qt()`` (the main\n        instantiator of this class), but it is ``True`` in ``napari.__main__``.\n        As a result, exceptions will be shown in the GUI only (mostly) when\n        running napari as ``napari`` or ``python -m napari`` from the command\n        line.\n    \"\"\"\n\n    error = Signal(tuple)\n    message: Optional[NapariNotification] = None\n\n    def __init__(self, parent=None, *, gui_exceptions=True):\n        super().__init__(parent)\n        if os.getenv(\"NAPARI_CATCH_ERRORS\") in ('0', 'False'):\n            self.gui_exceptions = False\n        else:\n            self.gui_exceptions = gui_exceptions\n\n    def handle(\n        self,\n        etype: Type[BaseException],\n        value: BaseException,\n        tb: TracebackType,\n    ):\n        \"\"\"Our sys.excepthook override.\n\n        This function handles uncaught exceptions and can delegate to a\n        secondary handler, whether it be a GUI dialog, or an IPython traceback\n        printout.  The override to ``sys.excepthook`` happens in\n        :func:`napari.gui_qt`, and therefore this is only active when the qt\n        event loop has been started by napari.\n\n        The three parameters here are what would be returned from\n        :func:`sys.exc_info()`.\n\n        Parameters\n        ----------\n        etype : Type[BaseException]\n            The type of error raised\n        value : BaseException\n            The error instance\n        tb : TracebackType\n            The traceback object associated with the error.\n        \"\"\"\n        if self.gui_exceptions:\n            self._show_error_dialog(value)\n        else:\n            text = \"\".join(traceback.format_exception(etype, value, tb))\n            logging.error(\"Unhandled exception:\\n%s\", text)\n        self.error.emit((etype, value, tb))\n\n    def _show_error_dialog(self, exception: BaseException):\n        self.message = NapariNotification.from_exception(exception)\n        self.message.show()\n", "path": "napari/_qt/exceptions.py"}], "after_files": [{"content": "import logging\nimport os\nimport sys\nimport traceback\nfrom types import TracebackType\nfrom typing import Optional, Type\n\nfrom qtpy.QtCore import QObject, Signal\n\nfrom .qt_error_notification import NapariNotification\n\n\nclass ExceptionHandler(QObject):\n    \"\"\"General class to handle all uncaught exceptions in the Qt event loop.\n\n    Parameters\n    ----------\n    parent : QObject, optional\n        parent object, by default None\n    gui_exceptions : bool, optional\n        Whether to show exceptions as, by default True.  May be overriden by\n        environment variable: ``NAPARI_CATCH_ERRORS=1`\n        Note: this ``False`` by default in ``gui_qt()`` (the main\n        instantiator of this class), but it is ``True`` in ``napari.__main__``.\n        As a result, exceptions will be shown in the GUI only (mostly) when\n        running napari as ``napari`` or ``python -m napari`` from the command\n        line.\n    \"\"\"\n\n    error = Signal(tuple)\n    message: Optional[NapariNotification] = None\n\n    def __init__(self, parent=None, *, gui_exceptions=True):\n        super().__init__(parent)\n        if os.getenv(\"NAPARI_CATCH_ERRORS\") in ('0', 'False'):\n            self.gui_exceptions = False\n        else:\n            self.gui_exceptions = gui_exceptions\n\n    def handle(\n        self,\n        etype: Type[BaseException],\n        value: BaseException,\n        tb: TracebackType,\n    ):\n        \"\"\"Our sys.excepthook override.\n\n        This function handles uncaught exceptions and can delegate to a\n        secondary handler, whether it be a GUI dialog, or an IPython traceback\n        printout.  The override to ``sys.excepthook`` happens in\n        :func:`napari.gui_qt`, and therefore this is only active when the qt\n        event loop has been started by napari.\n\n        The three parameters here are what would be returned from\n        :func:`sys.exc_info()`.\n\n        Parameters\n        ----------\n        etype : Type[BaseException]\n            The type of error raised\n        value : BaseException\n            The error instance\n        tb : TracebackType\n            The traceback object associated with the error.\n        \"\"\"\n        # etype.__module__ contains the module raising the error\n        # Custom exception classes can have different behavior\n        # can add custom exception handlers here ...\n        if isinstance(value, KeyboardInterrupt):\n            print(\"Closed by KeyboardInterrupt\", file=sys.stderr)\n            sys.exit(1)\n        if self.gui_exceptions:\n            self._show_error_dialog(value)\n        else:\n            text = \"\".join(traceback.format_exception(etype, value, tb))\n            logging.error(\"Unhandled exception:\\n%s\", text)\n        self.error.emit((etype, value, tb))\n\n    def _show_error_dialog(self, exception: BaseException):\n        self.message = NapariNotification.from_exception(exception)\n        self.message.show()\n", "path": "napari/_qt/exceptions.py"}]}