euporie 2.8.6__tar.gz → 2.8.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. {euporie-2.8.6 → euporie-2.8.7}/.gitignore +1 -142
  2. {euporie-2.8.6 → euporie-2.8.7}/LICENSE +1 -1
  3. {euporie-2.8.6 → euporie-2.8.7}/PKG-INFO +1 -1
  4. {euporie-2.8.6 → euporie-2.8.7}/euporie/console/app.py +2 -0
  5. {euporie-2.8.6 → euporie-2.8.7}/euporie/console/tabs/console.py +27 -17
  6. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/__init__.py +2 -2
  7. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/app/_commands.py +4 -21
  8. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/app/app.py +13 -7
  9. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/bars/command.py +9 -6
  10. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/bars/search.py +43 -2
  11. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/border.py +7 -2
  12. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/comm/base.py +2 -2
  13. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/comm/ipywidgets.py +3 -3
  14. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/commands.py +44 -8
  15. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/completion.py +14 -6
  16. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/datum.py +7 -7
  17. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/data_structures.py +20 -1
  18. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/filters.py +8 -0
  19. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/ft/html.py +47 -40
  20. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/graphics.py +11 -3
  21. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/history.py +15 -5
  22. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/inspection.py +16 -9
  23. euporie-2.8.7/euporie/core/kernel/__init__.py +53 -0
  24. euporie-2.8.7/euporie/core/kernel/base.py +571 -0
  25. euporie-2.8.6/euporie/core/kernel/client.py → euporie-2.8.7/euporie/core/kernel/jupyter.py +173 -430
  26. euporie-2.8.6/euporie/core/kernel/manager.py → euporie-2.8.7/euporie/core/kernel/jupyter_manager.py +4 -3
  27. euporie-2.8.7/euporie/core/kernel/local.py +694 -0
  28. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/bindings/basic.py +6 -3
  29. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/keys.py +26 -25
  30. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/layout/cache.py +31 -7
  31. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/layout/containers.py +88 -13
  32. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/layout/scroll.py +45 -148
  33. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/log.py +1 -1
  34. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/style.py +2 -1
  35. euporie-2.8.7/euporie/core/suggest.py +241 -0
  36. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/tabs/__init__.py +10 -0
  37. euporie-2.8.7/euporie/core/tabs/_commands.py +76 -0
  38. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/tabs/_settings.py +16 -0
  39. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/tabs/base.py +22 -8
  40. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/tabs/kernel.py +81 -35
  41. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/tabs/notebook.py +14 -22
  42. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/utils.py +1 -1
  43. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/validation.py +8 -8
  44. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/_settings.py +19 -2
  45. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/cell.py +31 -31
  46. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/cell_outputs.py +10 -1
  47. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/dialog.py +30 -75
  48. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/forms.py +71 -59
  49. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/inputs.py +7 -4
  50. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/layout.py +281 -93
  51. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/menu.py +55 -15
  52. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/palette.py +3 -1
  53. euporie-2.8.7/euporie/core/widgets/tree.py +119 -0
  54. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/app.py +35 -16
  55. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/tabs/edit.py +4 -4
  56. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/tabs/json.py +6 -2
  57. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/tabs/notebook.py +26 -8
  58. {euporie-2.8.6 → euporie-2.8.7}/euporie/preview/tabs/notebook.py +17 -13
  59. {euporie-2.8.6 → euporie-2.8.7}/euporie/web/tabs/web.py +22 -3
  60. {euporie-2.8.6 → euporie-2.8.7}/euporie/web/widgets/webview.py +3 -0
  61. {euporie-2.8.6 → euporie-2.8.7}/pyproject.toml +1 -1
  62. euporie-2.8.6/euporie/core/kernel/__init__.py +0 -1
  63. euporie-2.8.6/euporie/core/suggest.py +0 -160
  64. euporie-2.8.6/euporie/core/widgets/tree.py +0 -109
  65. {euporie-2.8.6 → euporie-2.8.7}/README.rst +0 -0
  66. {euporie-2.8.6 → euporie-2.8.7}/euporie/console/__init__.py +0 -0
  67. {euporie-2.8.6 → euporie-2.8.7}/euporie/console/__main__.py +0 -0
  68. {euporie-2.8.6 → euporie-2.8.7}/euporie/console/_commands.py +0 -0
  69. {euporie-2.8.6 → euporie-2.8.7}/euporie/console/_settings.py +0 -0
  70. {euporie-2.8.6 → euporie-2.8.7}/euporie/console/py.typed +0 -0
  71. {euporie-2.8.6 → euporie-2.8.7}/euporie/console/tabs/__init__.py +0 -0
  72. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/__main__.py +0 -0
  73. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/_settings.py +0 -0
  74. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/app/__init__.py +0 -0
  75. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/app/_settings.py +0 -0
  76. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/app/base.py +0 -0
  77. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/app/current.py +0 -0
  78. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/app/cursor.py +0 -0
  79. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/app/dummy.py +0 -0
  80. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/app/launch.py +0 -0
  81. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/bars/__init__.py +0 -0
  82. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/bars/menu.py +0 -0
  83. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/bars/status.py +0 -0
  84. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/clipboard.py +0 -0
  85. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/comm/__init__.py +0 -0
  86. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/comm/registry.py +0 -0
  87. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/config.py +0 -0
  88. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/__init__.py +0 -0
  89. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/__init__.py +0 -0
  90. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/ansi.py +0 -0
  91. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/base64.py +0 -0
  92. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/common.py +0 -0
  93. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/ft.py +0 -0
  94. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/html.py +0 -0
  95. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/jpeg.py +0 -0
  96. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/markdown.py +0 -0
  97. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/pdf.py +0 -0
  98. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/pil.py +0 -0
  99. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/png.py +0 -0
  100. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/rich.py +0 -0
  101. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/sixel.py +0 -0
  102. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/formats/svg.py +0 -0
  103. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/mime.py +0 -0
  104. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/registry.py +0 -0
  105. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/convert/utils.py +0 -0
  106. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/diagnostics.py +0 -0
  107. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/format.py +0 -0
  108. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/ft/__init__.py +0 -0
  109. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/ft/ansi.py +0 -0
  110. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/ft/table.py +0 -0
  111. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/ft/utils.py +0 -0
  112. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/io.py +0 -0
  113. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/__init__.py +0 -0
  114. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/bindings/__init__.py +0 -0
  115. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/bindings/completion.py +0 -0
  116. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/bindings/micro.py +0 -0
  117. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/bindings/mouse.py +0 -0
  118. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/bindings/page_navigation.py +0 -0
  119. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/bindings/terminal.py +0 -0
  120. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/bindings/vi.py +0 -0
  121. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/key_processor.py +0 -0
  122. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/micro_state.py +0 -0
  123. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/registry.py +0 -0
  124. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/utils.py +0 -0
  125. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/key_binding/vi_state.py +0 -0
  126. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/layout/__init__.py +0 -0
  127. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/layout/controls.py +0 -0
  128. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/layout/decor.py +0 -0
  129. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/layout/mouse.py +0 -0
  130. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/layout/print.py +0 -0
  131. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/layout/screen.py +0 -0
  132. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/lexers.py +0 -0
  133. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/lsp.py +0 -0
  134. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/margins.py +0 -0
  135. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/path.py +0 -0
  136. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/processors.py +0 -0
  137. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/py.typed +0 -0
  138. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/pygments.py +0 -0
  139. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/reference.py +0 -0
  140. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/renderer.py +0 -0
  141. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/__init__.py +0 -0
  142. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/decor.py +0 -0
  143. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/display.py +0 -0
  144. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/file_browser.py +0 -0
  145. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/formatted_text_area.py +0 -0
  146. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/logo.py +0 -0
  147. {euporie-2.8.6 → euporie-2.8.7}/euporie/core/widgets/pager.py +0 -0
  148. {euporie-2.8.6 → euporie-2.8.7}/euporie/data/desktop/euporie-console.desktop +0 -0
  149. {euporie-2.8.6 → euporie-2.8.7}/euporie/data/desktop/euporie-notebook.desktop +0 -0
  150. {euporie-2.8.6 → euporie-2.8.7}/euporie/hub/__init__.py +0 -0
  151. {euporie-2.8.6 → euporie-2.8.7}/euporie/hub/__main__.py +0 -0
  152. {euporie-2.8.6 → euporie-2.8.7}/euporie/hub/app.py +0 -0
  153. {euporie-2.8.6 → euporie-2.8.7}/euporie/hub/py.typed +0 -0
  154. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/__init__.py +0 -0
  155. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/__main__.py +0 -0
  156. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/_commands.py +0 -0
  157. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/_settings.py +0 -0
  158. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/current.py +0 -0
  159. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/enums.py +0 -0
  160. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/filters.py +0 -0
  161. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/py.typed +0 -0
  162. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/tabs/__init__.py +0 -0
  163. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/tabs/_commands.py +0 -0
  164. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/tabs/_settings.py +0 -0
  165. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/tabs/display.py +0 -0
  166. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/tabs/log.py +0 -0
  167. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/widgets/__init__.py +0 -0
  168. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/widgets/_commands.py +0 -0
  169. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/widgets/_settings.py +0 -0
  170. {euporie-2.8.6 → euporie-2.8.7}/euporie/notebook/widgets/side_bar.py +0 -0
  171. {euporie-2.8.6 → euporie-2.8.7}/euporie/preview/__init__.py +0 -0
  172. {euporie-2.8.6 → euporie-2.8.7}/euporie/preview/__main__.py +0 -0
  173. {euporie-2.8.6 → euporie-2.8.7}/euporie/preview/_settings.py +0 -0
  174. {euporie-2.8.6 → euporie-2.8.7}/euporie/preview/app.py +0 -0
  175. {euporie-2.8.6 → euporie-2.8.7}/euporie/preview/py.typed +0 -0
  176. {euporie-2.8.6 → euporie-2.8.7}/euporie/preview/tabs/__init__.py +0 -0
  177. {euporie-2.8.6 → euporie-2.8.7}/euporie/web/__init__.py +0 -0
  178. {euporie-2.8.6 → euporie-2.8.7}/euporie/web/tabs/__init__.py +0 -0
  179. {euporie-2.8.6 → euporie-2.8.7}/euporie/web/widgets/__init__.py +0 -0
@@ -2,9 +2,6 @@
2
2
  /media
3
3
  /examples
4
4
 
5
- # Python version specifier file
6
- .python-version
7
-
8
5
  # AI tools
9
6
  .aider*
10
7
 
@@ -97,7 +94,7 @@ ipython_config.py
97
94
  # pyenv
98
95
  # For a library or package, you might want to ignore these files since the code is
99
96
  # intended to run in multiple environments; otherwise, check them in:
100
- # .python-version
97
+ .python-version
101
98
 
102
99
  # pipenv
103
100
  # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@@ -151,141 +148,3 @@ dmypy.json
151
148
 
152
149
  # Cython debug symbols
153
150
  cython_debug/
154
- # Byte-compiled / optimized / DLL files
155
- __pycache__/
156
- *.py[cod]
157
- *$py.class
158
-
159
- # C extensions
160
- *.so
161
-
162
- # Distribution / packaging
163
- .Python
164
- build/
165
- develop-eggs/
166
- dist/
167
- downloads/
168
- eggs/
169
- .eggs/
170
- lib/
171
- lib64/
172
- parts/
173
- sdist/
174
- var/
175
- wheels/
176
- share/python-wheels/
177
- *.egg-info/
178
- .installed.cfg
179
- *.egg
180
- MANIFEST
181
-
182
- # PyInstaller
183
- # Usually these files are written by a python script from a template
184
- # before PyInstaller builds the exe, so as to inject date/other infos into it.
185
- *.manifest
186
- *.spec
187
-
188
- # Installer logs
189
- pip-log.txt
190
- pip-delete-this-directory.txt
191
-
192
- # Unit test / coverage reports
193
- htmlcov/
194
- .tox/
195
- .nox/
196
- .coverage
197
- .coverage.*
198
- .cache
199
- nosetests.xml
200
- coverage.xml
201
- *.cover
202
- *.py,cover
203
- .hypothesis/
204
- .pytest_cache/
205
- cover/
206
-
207
- # Translations
208
- *.mo
209
- *.pot
210
-
211
- # Django stuff:
212
- *.log
213
- local_settings.py
214
- db.sqlite3
215
- db.sqlite3-journal
216
-
217
- # Flask stuff:
218
- instance/
219
- .webassets-cache
220
-
221
- # Scrapy stuff:
222
- .scrapy
223
-
224
- # Sphinx documentation
225
- docs/_build/
226
-
227
- # PyBuilder
228
- .pybuilder/
229
- target/
230
-
231
- # Jupyter Notebook
232
- .ipynb_checkpoints
233
-
234
- # IPython
235
- profile_default/
236
- ipython_config.py
237
-
238
- # pyenv
239
- # For a library or package, you might want to ignore these files since the code is
240
- # intended to run in multiple environments; otherwise, check them in:
241
- # .python-version
242
-
243
- # pipenv
244
- # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
245
- # However, in case of collaboration, if having platform-specific dependencies or dependencies
246
- # having no cross-platform support, pipenv may install dependencies that don't work, or not
247
- # install all needed dependencies.
248
- #Pipfile.lock
249
-
250
- # PEP 582; used by e.g. github.com/David-OConnor/pyflow
251
- __pypackages__/
252
-
253
- # Celery stuff
254
- celerybeat-schedule
255
- celerybeat.pid
256
-
257
- # SageMath parsed files
258
- *.sage.py
259
-
260
- # Environments
261
- .env
262
- .venv
263
- env/
264
- venv/
265
- ENV/
266
- env.bak/
267
- venv.bak/
268
-
269
- # Spyder project settings
270
- .spyderproject
271
- .spyproject
272
-
273
- # Rope project settings
274
- .ropeproject
275
-
276
- # mkdocs documentation
277
- /site
278
-
279
- # mypy
280
- .mypy_cache/
281
- .dmypy.json
282
- dmypy.json
283
-
284
- # Pyre type checker
285
- .pyre/
286
-
287
- # pytype static type analyzer
288
- .pytype/
289
-
290
- # Cython debug symbols
291
- cython_debug/
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Josiah Outram Halstead
3
+ Copyright (c) 2025 Josiah Outram Halstead
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euporie
3
- Version: 2.8.6
3
+ Version: 2.8.7
4
4
  Summary: Euporie is a suite of terminal applications for interacting with Jupyter kernels
5
5
  Project-URL: Documentation, https://euporie.readthedocs.io/en/latest
6
6
  Project-URL: Issues, https://github.com/joouha/euporie/issues
@@ -32,6 +32,7 @@ from euporie.core.filters import has_dialog
32
32
  from euporie.core.layout.mouse import DisableMouseOnScroll
33
33
  from euporie.core.widgets.dialog import (
34
34
  AboutDialog,
35
+ ConfirmDialog,
35
36
  NoKernelsDialog,
36
37
  SaveAsDialog,
37
38
  SelectKernelDialog,
@@ -112,6 +113,7 @@ class ConsoleApp(BaseApp):
112
113
  self.dialogs["no-kernels"] = NoKernelsDialog(self)
113
114
  self.dialogs["change-kernel"] = SelectKernelDialog(self)
114
115
  self.dialogs["shortcuts"] = ShortcutsDialog(self)
116
+ self.dialogs["confirm"] = ConfirmDialog(self)
115
117
 
116
118
  return FloatContainer(
117
119
  DisableMouseOnScroll(
@@ -40,7 +40,7 @@ from euporie.core.filters import (
40
40
  )
41
41
  from euporie.core.format import LspFormatter
42
42
  from euporie.core.io import edit_in_editor
43
- from euporie.core.kernel.client import MsgCallbacks
43
+ from euporie.core.kernel.base import MsgCallbacks
44
44
  from euporie.core.key_binding.registry import (
45
45
  load_registered_bindings,
46
46
  register_bindings,
@@ -137,10 +137,7 @@ class Console(KernelTab):
137
137
 
138
138
  self.container = self.load_container()
139
139
 
140
- self.kernel.start(cb=self.kernel_started, wait=False)
141
-
142
140
  self.app.before_render += self.render_outputs
143
-
144
141
  self.on_advance = Event(self)
145
142
 
146
143
  async def load_lsps(self) -> None:
@@ -162,9 +159,26 @@ class Console(KernelTab):
162
159
 
163
160
  lsp.on_exit += lsp_unload
164
161
 
162
+ def post_init_kernel(self) -> None:
163
+ """Start the kernel after if has been loaded."""
164
+ # Load container
165
+ super().post_init_kernel()
166
+
167
+ # Start kernel
168
+ if self.kernel._status == "stopped":
169
+ self.kernel.start(cb=self.kernel_started, wait=False)
170
+
165
171
  def kernel_died(self) -> None:
166
- """Call when the kernel dies."""
172
+ """Call if the kernel dies."""
167
173
  log.error("The kernel has died")
174
+ if confirm := self.app.dialogs.get("confirm"):
175
+ confirm.show(
176
+ title="Kernel connection lost",
177
+ message="The kernel appears to have died\n"
178
+ "as it can no longer be reached.\n\n"
179
+ "Do you want to restart the kernel?",
180
+ cb=self.kernel.restart,
181
+ )
168
182
 
169
183
  async def load_history(self) -> None:
170
184
  """Load kernel history."""
@@ -191,9 +205,7 @@ class Console(KernelTab):
191
205
  def validate_input(self, code: str) -> bool:
192
206
  """Determine if the entered code is ready to run."""
193
207
  assert self.kernel is not None
194
- completeness_status = self.kernel.is_complete(code, wait=True).get(
195
- "status", "unknown"
196
- )
208
+ completeness_status = self.kernel.is_complete(code).get("status", "unknown")
197
209
  return not (
198
210
  not code.strip()
199
211
  or completeness_status == "incomplete"
@@ -404,12 +416,7 @@ class Console(KernelTab):
404
416
  if ((json_cells and cell.id != json_cells[0].id) or i > 0) and (
405
417
  (height_known and rows_above_layout > 0) or not height_known
406
418
  ):
407
- children.append(
408
- Window(
409
- height=1,
410
- dont_extend_height=True,
411
- )
412
- )
419
+ children.append(Window(height=1, dont_extend_height=True))
413
420
 
414
421
  # Cell input
415
422
  children.append(
@@ -441,9 +448,12 @@ class Console(KernelTab):
441
448
  if outputs := cell.outputs:
442
449
  # Add space before an output if last rendered cell did not have outputs
443
450
  # or we are rendering a new output
444
- if self.last_rendered is not None and (
445
- not self.last_rendered.outputs
446
- or cell.execution_count != self.last_rendered.execution_count
451
+ if self.last_rendered is None or (
452
+ self.last_rendered is not None
453
+ and (
454
+ not self.last_rendered.outputs
455
+ or cell.execution_count != self.last_rendered.execution_count
456
+ )
447
457
  ):
448
458
  children.append(
449
459
  Window(
@@ -1,10 +1,10 @@
1
1
  """This package defines the euporie application and its components."""
2
2
 
3
3
  __app_name__ = "euporie"
4
- __version__ = "2.8.6"
4
+ __version__ = "2.8.7"
5
5
  __logo__ = "⚈"
6
6
  __strapline__ = "Jupyter in the terminal"
7
7
  __author__ = "Josiah Outram Halstead"
8
8
  __email__ = "josiah@halstead.email"
9
- __copyright__ = f"© 2024, {__author__}"
9
+ __copyright__ = f"© 2025, {__author__}"
10
10
  __license__ = "MIT"
@@ -28,20 +28,6 @@ def _force_quit() -> None:
28
28
  Application.exit(get_app())
29
29
 
30
30
 
31
- @add_cmd(aliases=["wq", "x"])
32
- def _save_and_quit(event: KeyPressEvent) -> None:
33
- """Save the current tab then quits euporie."""
34
- from upath import UPath
35
-
36
- app = get_app()
37
- if (tab := get_app().tab) is not None:
38
- try:
39
- tab._save(UPath(event._arg) if event._arg else None)
40
- except NotImplementedError:
41
- pass
42
- app.exit()
43
-
44
-
45
31
  @add_cmd(aliases=["bc"], filter=tab_has_focus, menu_title="Close File")
46
32
  def _close_tab() -> None:
47
33
  """Close the current tab."""
@@ -79,17 +65,14 @@ def _clear_screen() -> None:
79
65
 
80
66
 
81
67
  @add_cmd(hidden=True, aliases=[""])
82
- def _go_to(event: KeyPressEvent) -> None:
68
+ def _go_to(event: KeyPressEvent, index: int = 0) -> None:
83
69
  """Go to a line or cell by number."""
84
- try:
85
- idx = int(event._arg or "") - 1
86
- except (ValueError, TypeError):
87
- return
70
+ index = max(0, index - 1)
88
71
  if buffer_has_focus():
89
72
  buffer = get_app().current_buffer
90
- buffer.cursor_position = len("".join(buffer.text.splitlines(True)[:idx]))
73
+ buffer.cursor_position = len("".join(buffer.text.splitlines(True)[:index]))
91
74
  elif tab_type_has_focus("euporie.notebook.tabs.notebook:Notebook")():
92
75
  from euporie.notebook.tabs.notebook import Notebook
93
76
 
94
77
  if isinstance(nb := get_app().tab, Notebook):
95
- nb.select(idx)
78
+ nb.select(index)
@@ -520,14 +520,17 @@ class BaseApp(ConfigurableApp, Application, ABC):
520
520
  @classmethod
521
521
  def launch(cls) -> None:
522
522
  """Launch the app."""
523
+ from prompt_toolkit.utils import in_main_thread
524
+
523
525
  super().launch()
524
526
  # Run the application
525
527
  with create_app_session(input=cls.load_input(), output=cls.load_output()):
526
528
  # Create an instance of the app and run it
527
529
  app = cls()
528
- # Handle SIGTERM while the app is running
529
- original_sigterm = signal.getsignal(signal.SIGTERM)
530
- signal.signal(signal.SIGTERM, app.cleanup)
530
+ if in_main_thread():
531
+ # Handle SIGTERM while the app is running
532
+ original_sigterm = signal.getsignal(signal.SIGTERM)
533
+ signal.signal(signal.SIGTERM, app.cleanup)
531
534
  # Set and run the app
532
535
  with set_app(app):
533
536
  try:
@@ -535,7 +538,8 @@ class BaseApp(ConfigurableApp, Application, ABC):
535
538
  except (EOFError, KeyboardInterrupt):
536
539
  result = None
537
540
  finally:
538
- signal.signal(signal.SIGTERM, original_sigterm)
541
+ if in_main_thread():
542
+ signal.signal(signal.SIGTERM, original_sigterm)
539
543
  # Shut down any remaining LSP clients at exit
540
544
  app.shutdown_lsps()
541
545
  return result
@@ -582,14 +586,16 @@ class BaseApp(ConfigurableApp, Application, ABC):
582
586
  path_mime = get_mime(path) or "text/plain"
583
587
  log.debug("File %s has mime type: %s", path, path_mime)
584
588
 
585
- tab_options: list[TabRegistryEntry] = []
589
+ # Use a set to automatically handle duplicates
590
+ tab_options: set[TabRegistryEntry] = set()
586
591
  for entry in self.tab_registry:
587
592
  for mime_type in entry.mime_types:
588
593
  if PurePath(path_mime).match(mime_type):
589
- tab_options.append(entry)
594
+ tab_options.add(entry)
590
595
  if path.suffix in entry.file_extensions:
591
- tab_options.append(entry)
596
+ tab_options.add(entry)
592
597
 
598
+ # Sort by weight (TabRegistryEntry.__lt__ handles this)
593
599
  return sorted(tab_options, reverse=True)
594
600
 
595
601
  def get_file_tab(self, path: Path) -> type[Tab] | None:
@@ -29,6 +29,7 @@ from euporie.core.key_binding.registry import (
29
29
 
30
30
  if TYPE_CHECKING:
31
31
  from collections.abc import Iterable
32
+ from typing import Unpack
32
33
 
33
34
  from prompt_toolkit.completion.base import CompleteEvent
34
35
  from prompt_toolkit.document import Document
@@ -195,11 +196,13 @@ class CommandBar:
195
196
 
196
197
  @staticmethod
197
198
  @add_cmd(aliases=["shell"])
198
- async def _run_shell_command(event: KeyPressEvent) -> None:
199
+ async def _run_shell_command(
200
+ event: KeyPressEvent, *cmd_arg: Unpack[tuple[str]]
201
+ ) -> None:
199
202
  """Run system command."""
200
- app = event.app
201
- if event._arg:
202
- await app.run_system_command(
203
- event._arg,
204
- display_before_text=[("bold", "$ "), ("", f"{event._arg}\n")],
203
+ command = " ".join(str(x) for x in cmd_arg)
204
+ if command:
205
+ await event.app.run_system_command(
206
+ command,
207
+ display_before_text=[("bold", "$ "), ("", f"{command}\n")],
205
208
  )
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ import re
6
7
  from typing import TYPE_CHECKING
7
8
 
8
9
  from prompt_toolkit.buffer import Buffer
@@ -27,6 +28,7 @@ from euporie.core.key_binding.registry import (
27
28
  if TYPE_CHECKING:
28
29
  from prompt_toolkit.filters import FilterOrBool
29
30
  from prompt_toolkit.formatted_text.base import AnyFormattedText
31
+ from prompt_toolkit.layout.controls import UIControl
30
32
 
31
33
  log = logging.getLogger(__name__)
32
34
 
@@ -119,9 +121,17 @@ def find_searchable_controls(
119
121
  search_buffer_control: SearchBufferControl, current_control: BufferControl | None
120
122
  ) -> list[BufferControl]:
121
123
  """Find list of searchable controls and the index of the next control."""
122
- searchable_controls: list[BufferControl] = []
124
+ # If a tab provides a list of buffers to search, use that. Otherwise, trawl the
125
+ # layout for buffer controls with this as its search control
126
+ long_list: list[UIControl]
127
+ if tab := get_app().tab:
128
+ try:
129
+ long_list = list(tab.__pt_searchables__())
130
+ except NotImplementedError:
131
+ long_list = list(get_app().layout.find_all_controls())
123
132
  next_control_index = 0
124
- for control in get_app().layout.find_all_controls():
133
+ searchable_controls: list[BufferControl] = []
134
+ for control in long_list:
125
135
  # Find the index of the next searchable control so we can link the search
126
136
  # control to it if the currently focused control is not searchable. This is so
127
137
  # that the next searchable control can be focused when search is completed.
@@ -134,6 +144,7 @@ def find_searchable_controls(
134
144
  ):
135
145
  # Add it to our list
136
146
  searchable_controls.append(control)
147
+ # Cut list based on current control index
137
148
  searchable_controls = (
138
149
  searchable_controls[next_control_index:]
139
150
  + searchable_controls[:next_control_index]
@@ -332,3 +343,33 @@ def accept_search() -> None:
332
343
  search_buffer_control.buffer.append_to_history()
333
344
  # Stop the search
334
345
  stop_search()
346
+
347
+
348
+ @add_cmd()
349
+ def _replace_all(find_str: str, replace_str: str) -> None:
350
+ """Find and replace text in all searchable buffers.
351
+
352
+ Args:
353
+ find_str: String pattern to find (will be converted to regex)
354
+ replace_str: Replacement string
355
+ """
356
+ # Convert find string to regex pattern
357
+ pattern = re.compile(find_str)
358
+
359
+ # Get searchable controls
360
+ search_buffer_control, current_control = find_search_control()
361
+ if search_buffer_control is None:
362
+ return
363
+ searchable_controls = find_searchable_controls(
364
+ search_buffer_control, current_control
365
+ )
366
+
367
+ # Apply replacements to each buffer
368
+ for control in searchable_controls:
369
+ if isinstance(control, BufferControl):
370
+ buffer = control.buffer
371
+ text = buffer.text
372
+ new_text = pattern.sub(replace_str, text)
373
+ if new_text != text:
374
+ buffer.text = new_text
375
+ buffer.on_text_changed()
@@ -512,6 +512,11 @@ _GRID_CHARS = {
512
512
  GridChar(LowerLeftQuarterLine, NoLine, NoLine, UpperRightEighthLine): " ",
513
513
  GridChar(UpperRightQuarterLine, UpperRightEighthLine, NoLine, NoLine): " ",
514
514
 
515
+ GridChar(NoLine, NoLine, UpperRightQuarterLine, UpperRightEighthLine): "▁",
516
+ GridChar(UpperRightQuarterLine, NoLine, NoLine, UpperRightEighthLine): "▔",
517
+ GridChar(LowerLeftQuarterLine, UpperRightEighthLine, NoLine, NoLine): "▔",
518
+ GridChar(NoLine, LowerLeftEighthLine, LowerLeftQuarterLine, NoLine): "▁",
519
+
515
520
  # LowerLeftQuarterLine
516
521
  GridChar(LowerLeftQuarterLine, NoLine, LowerLeftQuarterLine, NoLine): "▎",
517
522
  GridChar(NoLine, LowerLeftQuarterLine, NoLine, LowerLeftQuarterLine): "▂",
@@ -875,9 +880,9 @@ InsetGrid = (
875
880
 
876
881
  OutsetGrid = (
877
882
  LowerLeftEighthLine.top_edge
878
- + UpperRightEighthLine.right_edge
883
+ + UpperRightQuarterLine.right_edge
879
884
  + UpperRightEighthLine.bottom_edge
880
- + LowerLeftEighthLine.left_edge
885
+ + LowerLeftQuarterLine.left_edge
881
886
  + ThinLine.inner
882
887
  )
883
888
 
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
 
16
16
  from prompt_toolkit.layout.containers import AnyContainer
17
17
 
18
- from euporie.core.kernel.client import Kernel
18
+ from euporie.core.kernel.jupyter import JupyterKernel
19
19
  from euporie.core.tabs.kernel import KernelTab
20
20
  from euporie.core.widgets.cell_outputs import OutputParent
21
21
 
@@ -40,7 +40,7 @@ class CommView:
40
40
  """
41
41
  self.container = container
42
42
  self.setters: dict[str, Callable[..., None]] = dict(setters or {})
43
- self.kernel: Kernel | None = None
43
+ self.kernel: JupyterKernel | None = None
44
44
 
45
45
  def update(self, changes: dict[str, Any]) -> None:
46
46
  """Update the view to reflect changes in the Comm.
@@ -19,7 +19,7 @@ from prompt_toolkit.layout.processors import BeforeInput
19
19
 
20
20
  from euporie.core.comm.base import Comm, CommView
21
21
  from euporie.core.data_structures import DiBool
22
- from euporie.core.kernel.client import MsgCallbacks
22
+ from euporie.core.kernel.jupyter import MsgCallbacks
23
23
  from euporie.core.layout.decor import FocusedStyle
24
24
  from euporie.core.widgets.forms import (
25
25
  Button,
@@ -518,7 +518,7 @@ class TextBoxIpyWidgetComm(IpyWidgetComm, metaclass=ABCMeta):
518
518
  container,
519
519
  setters={
520
520
  "value": lambda x: setattr(text.buffer, "text", str(x)),
521
- "rows": partial(setattr, text.text_area.window, "height"),
521
+ "rows": partial(setattr, text.window, "height"),
522
522
  "placeholder": partial(setattr, text, "placeholder"),
523
523
  "description_allow_html": partial(setattr, labelled_widget, "html"),
524
524
  },
@@ -1403,7 +1403,7 @@ class ColorPickerModel(TextBoxIpyWidgetComm):
1403
1403
  container,
1404
1404
  setters={
1405
1405
  "value": lambda x: setattr(text.buffer, "text", str(x)),
1406
- "rows": partial(setattr, text.text_area.window, "height"),
1406
+ "rows": partial(setattr, text.window, "height"),
1407
1407
  "placeholder": partial(setattr, text, "placeholder"),
1408
1408
  },
1409
1409
  )
@@ -41,6 +41,36 @@ if TYPE_CHECKING:
41
41
  log = logging.getLogger(__name__)
42
42
 
43
43
 
44
+ def parse_args(arg: str) -> list[Any]:
45
+ """Parse a command argument string into a list of values.
46
+
47
+ Args:
48
+ arg: The argument string to parse
49
+
50
+ Returns:
51
+ A list of parsed values, with strings for items that couldn't be evaluated
52
+ """
53
+ if not arg:
54
+ return []
55
+
56
+ import ast
57
+
58
+ result = []
59
+ for item in arg.split():
60
+ try:
61
+ # Safely evaluate string as a Python literal
62
+ new_value = ast.literal_eval(item)
63
+ except (ValueError, SyntaxError):
64
+ # Keep as string if evaluation fails
65
+ result.append(item)
66
+ else:
67
+ if type(new_value) is str:
68
+ result.append(item)
69
+ else:
70
+ result.append(new_value)
71
+ return result
72
+
73
+
44
74
  class Command:
45
75
  """Wrap a function so it can be used as a key-binding or a menu item."""
46
76
 
@@ -110,18 +140,19 @@ class Command:
110
140
 
111
141
  self.keys: list[tuple[str | Keys, ...]] = []
112
142
 
113
- def run(self, arg: str | None = None) -> None:
143
+ def run(self, arg: str = "") -> None:
114
144
  """Run the command's handler."""
115
145
  if self.filter():
116
146
  app = get_app()
117
147
  result = self.key_handler(
118
148
  KeyPressEvent(
119
149
  key_processor_ref=weakref.ref(app.key_processor),
120
- arg=arg,
150
+ arg=None,
121
151
  key_sequence=[],
122
152
  previous_key_sequence=[],
123
153
  is_repeat=False,
124
154
  ),
155
+ *parse_args(arg),
125
156
  )
126
157
  if isawaitable(result):
127
158
 
@@ -141,14 +172,19 @@ class Command:
141
172
  handler = self.handler
142
173
  sig = signature(handler)
143
174
 
144
- if sig.parameters:
145
- # The handler already accepts a `KeyPressEvent` argument
175
+ if sig.parameters and next(iter(sig.parameters.keys())) == "event":
176
+ # The handler already accepts a `KeyPressEvent` argument named "event"
177
+ # as the first parameter
146
178
  return cast("KeyHandlerCallable", handler)
147
179
 
180
+ # Otherwise we need to wrap in a function which accepts a KeyPressEvent as the
181
+ # first parameter
148
182
  if iscoroutinefunction(handler):
149
183
 
150
- async def _key_handler_async(event: KeyPressEvent) -> NotImplementedOrNone:
151
- result = cast("CommandHandlerNoArgs", handler)()
184
+ async def _key_handler_async(
185
+ event: KeyPressEvent, *args: Any
186
+ ) -> NotImplementedOrNone:
187
+ result = cast("CommandHandlerNoArgs", handler)(*args)
152
188
  assert isawaitable(result)
153
189
  return await result
154
190
 
@@ -156,8 +192,8 @@ class Command:
156
192
 
157
193
  else:
158
194
 
159
- def _key_handler(event: KeyPressEvent) -> NotImplementedOrNone:
160
- return cast("CommandHandlerNoArgs", handler)()
195
+ def _key_handler(event: KeyPressEvent, *args: Any) -> NotImplementedOrNone:
196
+ return cast("CommandHandlerNoArgs", handler)(*args)
161
197
 
162
198
  return _key_handler
163
199