euporie 2.6.1__py3-none-any.whl → 2.7.0__py3-none-any.whl

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 (67) hide show
  1. euporie/console/tabs/console.py +51 -43
  2. euporie/core/__init__.py +5 -2
  3. euporie/core/app.py +74 -57
  4. euporie/core/comm/ipywidgets.py +7 -3
  5. euporie/core/config.py +51 -27
  6. euporie/core/convert/__init__.py +2 -0
  7. euporie/core/convert/datum.py +82 -45
  8. euporie/core/convert/formats/ansi.py +1 -2
  9. euporie/core/convert/formats/common.py +7 -11
  10. euporie/core/convert/formats/ft.py +10 -7
  11. euporie/core/convert/formats/png.py +7 -6
  12. euporie/core/convert/formats/sixel.py +1 -1
  13. euporie/core/convert/formats/svg.py +28 -0
  14. euporie/core/convert/mime.py +4 -7
  15. euporie/core/data_structures.py +24 -22
  16. euporie/core/filters.py +16 -2
  17. euporie/core/format.py +30 -4
  18. euporie/core/ft/ansi.py +2 -1
  19. euporie/core/ft/html.py +155 -42
  20. euporie/core/{widgets/graphics.py → graphics.py} +225 -227
  21. euporie/core/io.py +8 -0
  22. euporie/core/key_binding/bindings/__init__.py +8 -2
  23. euporie/core/key_binding/bindings/basic.py +9 -14
  24. euporie/core/key_binding/bindings/micro.py +0 -12
  25. euporie/core/key_binding/bindings/mouse.py +107 -80
  26. euporie/core/key_binding/bindings/page_navigation.py +129 -0
  27. euporie/core/key_binding/key_processor.py +9 -1
  28. euporie/core/layout/__init__.py +1 -0
  29. euporie/core/layout/containers.py +1011 -0
  30. euporie/core/layout/decor.py +381 -0
  31. euporie/core/layout/print.py +130 -0
  32. euporie/core/layout/screen.py +75 -0
  33. euporie/core/{widgets/page.py → layout/scroll.py} +166 -111
  34. euporie/core/log.py +1 -1
  35. euporie/core/margins.py +11 -5
  36. euporie/core/path.py +43 -176
  37. euporie/core/renderer.py +31 -8
  38. euporie/core/style.py +2 -0
  39. euporie/core/tabs/base.py +2 -1
  40. euporie/core/terminal.py +19 -21
  41. euporie/core/widgets/cell.py +2 -4
  42. euporie/core/widgets/cell_outputs.py +2 -2
  43. euporie/core/widgets/decor.py +3 -359
  44. euporie/core/widgets/dialog.py +5 -5
  45. euporie/core/widgets/display.py +32 -12
  46. euporie/core/widgets/file_browser.py +3 -4
  47. euporie/core/widgets/forms.py +36 -14
  48. euporie/core/widgets/inputs.py +171 -99
  49. euporie/core/widgets/layout.py +80 -5
  50. euporie/core/widgets/menu.py +1 -3
  51. euporie/core/widgets/pager.py +3 -3
  52. euporie/core/widgets/palette.py +3 -2
  53. euporie/core/widgets/status_bar.py +2 -6
  54. euporie/core/widgets/tree.py +3 -6
  55. euporie/notebook/app.py +8 -8
  56. euporie/notebook/tabs/notebook.py +2 -2
  57. euporie/notebook/widgets/side_bar.py +1 -1
  58. euporie/preview/tabs/notebook.py +2 -2
  59. euporie/web/tabs/web.py +6 -1
  60. euporie/web/widgets/webview.py +52 -32
  61. {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/METADATA +9 -11
  62. {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/RECORD +67 -60
  63. {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/WHEEL +1 -1
  64. {euporie-2.6.1.data → euporie-2.7.0.data}/data/share/applications/euporie-console.desktop +0 -0
  65. {euporie-2.6.1.data → euporie-2.7.0.data}/data/share/applications/euporie-notebook.desktop +0 -0
  66. {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/entry_points.txt +0 -0
  67. {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,6 @@ from functools import partial
7
7
  from typing import TYPE_CHECKING, cast
8
8
 
9
9
  import nbformat
10
- from prompt_toolkit.application.run_in_terminal import in_terminal
11
10
  from prompt_toolkit.buffer import Buffer, ValidationState
12
11
  from prompt_toolkit.filters.app import (
13
12
  buffer_has_focus,
@@ -44,13 +43,13 @@ from euporie.core.key_binding.registry import (
44
43
  load_registered_bindings,
45
44
  register_bindings,
46
45
  )
46
+ from euporie.core.layout.print import PrintingContainer
47
47
  from euporie.core.style import KERNEL_STATUS_REPR
48
48
  from euporie.core.tabs.base import KernelTab
49
49
  from euporie.core.terminal import edit_in_editor
50
50
  from euporie.core.validation import KernelValidator
51
51
  from euporie.core.widgets.cell_outputs import CellOutputArea
52
52
  from euporie.core.widgets.inputs import KernelInput, StdInput
53
- from euporie.core.widgets.page import PrintingContainer
54
53
  from euporie.core.widgets.pager import PagerState
55
54
 
56
55
  if TYPE_CHECKING:
@@ -177,8 +176,9 @@ class Console(KernelTab):
177
176
  """Run the code in the input box."""
178
177
  if buffer is None:
179
178
  buffer = self.input_box.buffer
179
+ app = self.app
180
180
  # Auto-reformat code
181
- if self.app.config.autoformat:
181
+ if app.config.autoformat:
182
182
  self.reformat()
183
183
  # Get the code to run
184
184
  text = buffer.text
@@ -187,9 +187,12 @@ class Console(KernelTab):
187
187
  # Disable existing output
188
188
  self.live_output.style = "class:disabled"
189
189
  # Re-render the app and move to below the current output
190
- self.app.draw()
190
+ original_layout = app.layout
191
+ app.layout = self.input_layout
192
+ app.draw()
193
+ app.layout = original_layout
191
194
  # Prevent displayed graphics on terminal being cleaned up (bit of a hack)
192
- self.app.graphics.clear()
195
+ app.graphics.clear()
193
196
  # Run the previous entry
194
197
  if self.kernel.status == "starting":
195
198
  self.kernel_queue.append(partial(self.kernel.run, text, wait=False))
@@ -201,15 +204,15 @@ class Console(KernelTab):
201
204
  buffer.reset(append_to_history=True)
202
205
  # Remove any live outputs and disable mouse support
203
206
  self.live_output.reset()
204
- if self.app.config.mouse_support is None:
205
- self.app.need_mouse_support = False
207
+ if app.config.mouse_support is None:
208
+ app.need_mouse_support = False
206
209
  # Record the input as a cell in the json
207
210
  self.json["cells"].append(
208
211
  nbformat.v4.new_code_cell(source=text, execution_count=self.execution_count)
209
212
  )
210
213
  if (
211
- self.app.config.max_stored_outputs
212
- and len(self.json["cells"]) > self.app.config.max_stored_outputs
214
+ app.config.max_stored_outputs
215
+ and len(self.json["cells"]) > app.config.max_stored_outputs
213
216
  ):
214
217
  del self.json["cells"][0]
215
218
 
@@ -241,14 +244,12 @@ class Console(KernelTab):
241
244
  def render_outputs(self, app: Application[Any]) -> None:
242
245
  """Request that any unrendered outputs be rendered."""
243
246
  if self.output.json:
244
- self.app.create_background_task(self.async_render_outputs())
245
-
246
- async def async_render_outputs(self) -> None:
247
- """Render any unrendered outputs above the application."""
248
- if self.output.json:
249
- # Run the output app in the terminal
250
- async with in_terminal():
251
- self.app.renderer.render(self.app, self.output_layout, is_done=True)
247
+ app = self.app
248
+ original_layout = self.app.layout
249
+ app.layout = self.output_layout
250
+ app.renderer.render(self.app, self.output_layout, is_done=True)
251
+ app.renderer.request_absolute_cursor_position()
252
+ app.layout = original_layout
252
253
  # Remove the outputs so they do not get rendered again
253
254
  self.output.reset()
254
255
 
@@ -410,9 +411,13 @@ class Console(KernelTab):
410
411
  height=1,
411
412
  )
412
413
 
414
+ def _handler(buffer: Buffer) -> bool:
415
+ self.run(buffer)
416
+ return True
417
+
413
418
  self.input_box = KernelInput(
414
419
  kernel_tab=self,
415
- accept_handler=self.run,
420
+ accept_handler=_handler,
416
421
  on_cursor_position_changed=on_cursor_position_changed,
417
422
  validator=KernelValidator(self.kernel),
418
423
  # validate_while_typing=False,
@@ -424,8 +429,34 @@ class Console(KernelTab):
424
429
  self.app.focused_element = self.input_box.buffer
425
430
 
426
431
  self.stdin_box = StdInput(self)
432
+ input_row = [
433
+ # Spacing
434
+ ConditionalContainer(
435
+ Window(height=1, dont_extend_height=True),
436
+ filter=Condition(lambda: self.execution_count > 0)
437
+ & (
438
+ (
439
+ renderer_height_is_known
440
+ & Condition(lambda: self.app.renderer.rows_above_layout > 0)
441
+ )
442
+ | ~renderer_height_is_known
443
+ ),
444
+ ),
445
+ # Input
446
+ ConditionalContainer(
447
+ VSplit(
448
+ [
449
+ input_prompt,
450
+ self.input_box,
451
+ ],
452
+ ),
453
+ filter=~self.stdin_box.visible,
454
+ ),
455
+ ]
456
+
457
+ self.input_layout = Layout(PrintingContainer(input_row))
427
458
 
428
- self.input_layout = HSplit(
459
+ return HSplit(
429
460
  [
430
461
  ConditionalContainer(
431
462
  HSplit(
@@ -442,36 +473,13 @@ class Console(KernelTab):
442
473
  Window(height=1, dont_extend_height=True),
443
474
  filter=self.stdin_box.visible,
444
475
  ),
445
- # Spacing
446
- ConditionalContainer(
447
- Window(height=1, dont_extend_height=True),
448
- filter=Condition(lambda: self.execution_count > 0)
449
- & (
450
- (
451
- renderer_height_is_known
452
- & Condition(lambda: self.app.renderer.rows_above_layout > 0)
453
- )
454
- | ~renderer_height_is_known
455
- ),
456
- ),
457
- # Input
458
- ConditionalContainer(
459
- VSplit(
460
- [
461
- input_prompt,
462
- self.input_box,
463
- ],
464
- ),
465
- filter=~self.stdin_box.visible,
466
- ),
476
+ *input_row,
467
477
  ],
468
478
  key_bindings=load_registered_bindings(
469
479
  "euporie.console.tabs.console.Console"
470
480
  ),
471
481
  )
472
482
 
473
- return self.input_layout
474
-
475
483
  def set_next_input(self, text: str, replace: bool = False) -> None:
476
484
  """Set the text for the next prompt."""
477
485
  self.input_box.buffer.text = text
euporie/core/__init__.py CHANGED
@@ -1,15 +1,18 @@
1
1
  """This package defines the euporie application and its components."""
2
2
 
3
3
  __app_name__ = "euporie"
4
- __version__ = "2.6.1"
4
+ __version__ = "2.7.0"
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"© 2022, {__author__}"
9
+ __copyright__ = f"© 2023, {__author__}"
10
10
  __license__ = "MIT"
11
11
 
12
12
 
13
13
  # Register extensions to external packages
14
14
  from euporie.core import path # noqa F401
15
15
  from euporie.core import pygments # noqa F401
16
+
17
+ # Monkey-patch prompt_toolkit
18
+ from euporie.core.layout import containers # noqa: F401
euporie/core/app.py CHANGED
@@ -39,7 +39,7 @@ from prompt_toolkit.key_binding.key_bindings import (
39
39
  ConditionalKeyBindings,
40
40
  merge_key_bindings,
41
41
  )
42
- from prompt_toolkit.layout.containers import Float, FloatContainer, Window, to_container
42
+ from prompt_toolkit.layout.containers import Float, FloatContainer, to_container
43
43
  from prompt_toolkit.layout.layout import Layout
44
44
  from prompt_toolkit.output import ColorDepth
45
45
  from prompt_toolkit.output.defaults import create_output
@@ -64,7 +64,7 @@ from euporie.core.commands import add_cmd
64
64
  from euporie.core.config import Config, add_setting
65
65
  from euporie.core.convert.mime import get_mime
66
66
  from euporie.core.current import get_app
67
- from euporie.core.filters import in_tmux, insert_mode, replace_mode, tab_has_focus
67
+ from euporie.core.filters import in_mplex, insert_mode, replace_mode, tab_has_focus
68
68
  from euporie.core.io import Vt100_Output, Vt100Parser
69
69
  from euporie.core.key_binding.key_processor import KeyProcessor
70
70
  from euporie.core.key_binding.micro_state import MicroState
@@ -73,6 +73,7 @@ from euporie.core.key_binding.registry import (
73
73
  register_bindings,
74
74
  )
75
75
  from euporie.core.key_binding.vi_state import ViState
76
+ from euporie.core.layout.containers import Window
76
77
  from euporie.core.log import setup_logs
77
78
  from euporie.core.path import parse_path
78
79
  from euporie.core.renderer import Renderer
@@ -194,6 +195,7 @@ class BaseApp(Application):
194
195
  leave_graphics: FilterOrBool = True,
195
196
  extend_renderer_height: FilterOrBool = False,
196
197
  extend_renderer_width: FilterOrBool = False,
198
+ enable_page_navigation_bindings: FilterOrBool | None = True,
197
199
  **kwargs: Any,
198
200
  ) -> None:
199
201
  """Instantiate euporie specific application variables.
@@ -210,6 +212,8 @@ class BaseApp(Application):
210
212
  beyond the height of the display
211
213
  extend_renderer_width: Whether the renderer width should be extended
212
214
  beyond the height of the display
215
+ enable_page_navigation_bindings: Determines if page navigation keybindings
216
+ should be loaded
213
217
  kwargs: The key-word arguments for the :py:class:`Application`
214
218
 
215
219
  """
@@ -220,6 +224,7 @@ class BaseApp(Application):
220
224
  "editing_mode": self.get_edit_mode(),
221
225
  "mouse_support": Condition(lambda: self.need_mouse_support),
222
226
  "cursor": CursorConfig(),
227
+ "enable_page_navigation_bindings": enable_page_navigation_bindings,
223
228
  **kwargs,
224
229
  }
225
230
  )
@@ -270,6 +275,12 @@ class BaseApp(Application):
270
275
  "euporie.core.app.BaseApp",
271
276
  "euporie.core.terminal.TerminalInfo",
272
277
  ]
278
+
279
+ from euporie.core.key_binding.bindings.page_navigation import (
280
+ load_page_navigation_bindings,
281
+ )
282
+
283
+ self._page_navigation_bindings = load_page_navigation_bindings(self.config)
273
284
  # Determines which clipboard mechanism to use
274
285
  self.clipboard: Clipboard = EuporieClipboard(self)
275
286
  # Allow hiding element when manually redrawing app
@@ -365,10 +376,8 @@ class BaseApp(Application):
365
376
  with set_app(self):
366
377
  # Load key bindings
367
378
  self.load_key_bindings()
368
-
369
379
  # Send queries to the terminal
370
380
  self.term_info.send_all()
371
-
372
381
  # Read responses
373
382
  kp = self.key_processor
374
383
 
@@ -376,9 +385,9 @@ class BaseApp(Application):
376
385
  kp.feed_multiple(self.input.read_keys())
377
386
 
378
387
  with self.input.raw_mode(), self.input.attach(read_from_input):
379
- # Wait up to half a second for the terminal to respond to queries
380
- await asyncio.sleep(0.5)
381
-
388
+ # Give the terminal time to respond and allow the event loop to read
389
+ # the terminal responses from the input
390
+ await asyncio.sleep(0.1)
382
391
  kp.process_keys()
383
392
 
384
393
  return await super().run_async(
@@ -495,12 +504,10 @@ class BaseApp(Application):
495
504
  setup_logs()
496
505
  # Load the app's configuration
497
506
  cls.config.load(cls)
498
- # Configure the logs
499
- setup_logs(cls.config)
500
- # Warn about unrecognised configuration items
501
- cls.config.warn()
502
507
  # Run the application
503
- with create_app_session(input=cls.load_input(), output=cls.load_output()):
508
+ with create_app_session(
509
+ input=cls.load_input(), output=(output := cls.load_output())
510
+ ):
504
511
  # Create an instance of the app and run it
505
512
 
506
513
  original_sigterm = signal.getsignal(signal.SIGTERM)
@@ -516,7 +523,10 @@ class BaseApp(Application):
516
523
  signal.signal(signal.SIGTERM, original_sigterm)
517
524
  signal.signal(signal.SIGINT, original_sigint)
518
525
 
519
- return result
526
+ # This seems to be needed for kitty
527
+ output.enable_autowrap()
528
+
529
+ return result
520
530
 
521
531
  def cleanup(self, signum: int, frame: FrameType | None) -> None:
522
532
  """Restore the state of the terminal on unexpected exit."""
@@ -524,13 +534,9 @@ class BaseApp(Application):
524
534
  output = self.output
525
535
  self.exit()
526
536
  # Reset terminal state
527
- # output.quit_alternate_screen()
528
- # output.disable_mouse_support()
529
537
  output.reset_cursor_key_mode()
530
538
  output.enable_autowrap()
531
- # output.disable_bracketed_paste()
532
539
  output.clear_title()
533
- # output.reset_cursor_shape()
534
540
  output.show_cursor()
535
541
  output.reset_attributes()
536
542
  self.renderer.reset()
@@ -738,6 +744,16 @@ class BaseApp(Application):
738
744
  "default" if name in ("fg", "bg") else name,
739
745
  )
740
746
  # Add accent color
747
+ # self.color_palette.add_color(
748
+ # "hl",
749
+ # (bg := self.color_palette.bg)
750
+ # .adjust(
751
+ # hue=(bg.hue + (bg.hue - 0.036)) % 1,
752
+ # saturation=(0.88 - bg.saturation),
753
+ # brightness=0.4255 - bg.brightness,
754
+ # )
755
+ # .base_hex,
756
+ # )
741
757
  self.color_palette.add_color(
742
758
  "hl", base_colors.get(self.config.accent_color, self.config.accent_color)
743
759
  )
@@ -778,6 +794,8 @@ class BaseApp(Application):
778
794
  ) -> None:
779
795
  """Update the application's style when the syntax theme is changed."""
780
796
  self.renderer.style = self.create_merged_style()
797
+ # Trigger a re-draw
798
+ self.invalidate()
781
799
 
782
800
  def refresh(self) -> None:
783
801
  """Reet all tabs."""
@@ -933,11 +951,11 @@ class BaseApp(Application):
933
951
  add_setting(
934
952
  name="terminal_polling_interval",
935
953
  flags=["--terminal-polling-interval"],
936
- type_=int,
954
+ type_=float,
937
955
  help_="Time between terminal colour queries",
938
- default=0,
956
+ default=0.0,
939
957
  schema={
940
- "min": 0,
958
+ "min": 0.0,
941
959
  },
942
960
  description="""
943
961
  Determine how frequently the terminal should be polled for changes to the
@@ -957,35 +975,27 @@ class BaseApp(Application):
957
975
  )
958
976
 
959
977
  add_setting(
960
- name="format_black",
961
- flags=["--format-black"],
962
- type_=bool,
963
- help_="Use black when re-formatting code cells",
964
- default=True,
965
- description="""
966
- Whether to use :py:mod:`black` when reformatting code cells.
967
- """,
968
- )
969
-
970
- add_setting(
971
- name="format_isort",
972
- flags=["--format-isort"],
973
- type_=bool,
974
- help_="Use isort when re-formatting code cells",
975
- default=True,
976
- description="""
977
- Whether to use :py:mod:`isort` when reformatting code cells.
978
- """,
979
- )
980
-
981
- add_setting(
982
- name="format_ssort",
983
- flags=["--format-ssort"],
984
- type_=bool,
985
- help_="Use ssort when re-formatting code cells",
986
- default=True,
978
+ name="formatters",
979
+ flags=["--formatters"],
980
+ type_=str,
981
+ choices=["ruff", "black", "isort", "ssort"],
982
+ help_="List formatters to use when re-formatting code cells",
983
+ default=["ruff"],
984
+ action="append",
985
+ schema={
986
+ "type": "array",
987
+ "items": {
988
+ "description": "Formatters",
989
+ "type": "string",
990
+ },
991
+ },
987
992
  description="""
988
- Whether to use :py:mod:`ssort` when reformatting code cells.
993
+ A list of the names of the formatters to use when reformatting code cells.
994
+ Supported formatters include:
995
+ - :py:mod:`ruff`
996
+ - :py:mod:`black`
997
+ - :py:mod:`isort`
998
+ - :py:mod:`ssort`
989
999
  """,
990
1000
  )
991
1001
 
@@ -1020,24 +1030,31 @@ class BaseApp(Application):
1020
1030
  )
1021
1031
 
1022
1032
  add_setting(
1023
- name="tmux_graphics",
1024
- flags=["--tmux-graphics"],
1033
+ name="multiplexer_passthrough",
1034
+ flags=["--multiplexer-passthrough"],
1025
1035
  type_=bool,
1026
- help_="Enable terminal graphics in tmux (experimental)",
1036
+ help_="Use passthrough from within terminal multiplexers",
1027
1037
  default=False,
1028
- hidden=~in_tmux,
1038
+ hidden=~in_mplex,
1029
1039
  description="""
1030
- If set, terminal graphics will be used if :program:`tmux` is running by
1031
- performing terminal escape sequence pass-through. You must restart euporie
1032
- for this to take effect.
1040
+ If set and euporie is running inside a terminal multiplexer
1041
+ (:program:`screen` or :program:`tmux`), then certain escape sequences
1042
+ will be passed-through the multiplexer directly to the terminal.
1033
1043
 
1034
- You will also need to ensure that ``allow-passthrough`` is set to ``on`` in
1035
- your :program:`tmux` configuration.
1044
+ This affects things such as terminal color detection and graphics display.
1045
+
1046
+ for tmux, you will also need to ensure that ``allow-passthrough`` is set to
1047
+ ``on`` in your :program:`tmux` configuration.
1036
1048
 
1037
1049
  .. warning::
1038
1050
 
1039
1051
  Terminal graphics in :program:`tmux` is experimental, and is not
1040
1052
  guaranteed to work. Use at your own risk!
1053
+
1054
+ .. note::
1055
+ As of version :command:`tmux` version ``3.4`` sixel graphics are
1056
+ supported, which may result in better terminal graphics then using
1057
+ multiplexer passthrough.
1041
1058
  """,
1042
1059
  )
1043
1060
 
@@ -16,14 +16,13 @@ from typing import TYPE_CHECKING
16
16
  from prompt_toolkit.filters.base import Condition
17
17
  from prompt_toolkit.layout.containers import HSplit, VSplit
18
18
  from prompt_toolkit.layout.processors import BeforeInput
19
- from prompt_toolkit.widgets.base import Box
20
19
 
21
20
  from euporie.core.comm.base import Comm, CommView
22
21
  from euporie.core.convert.datum import Datum
23
22
  from euporie.core.data_structures import DiBool
24
23
  from euporie.core.kernel import MsgCallbacks
24
+ from euporie.core.layout.decor import FocusedStyle
25
25
  from euporie.core.widgets.cell_outputs import CellOutputArea
26
- from euporie.core.widgets.decor import FocusedStyle
27
26
  from euporie.core.widgets.display import Display
28
27
  from euporie.core.widgets.forms import (
29
28
  Button,
@@ -39,7 +38,12 @@ from euporie.core.widgets.forms import (
39
38
  ToggleButton,
40
39
  ToggleButtons,
41
40
  )
42
- from euporie.core.widgets.layout import AccordionSplit, ReferencedSplit, TabbedSplit
41
+ from euporie.core.widgets.layout import (
42
+ AccordionSplit,
43
+ Box,
44
+ ReferencedSplit,
45
+ TabbedSplit,
46
+ )
43
47
 
44
48
  if TYPE_CHECKING:
45
49
  from typing import Any, Iterable, MutableSequence, Sequence
euporie/core/config.py CHANGED
@@ -45,7 +45,7 @@ _SCHEMA_TYPES: dict[type | Callable, str] = {
45
45
  bool: "boolean",
46
46
  str: "string",
47
47
  int: "integer",
48
- float: "float",
48
+ float: "number",
49
49
  UPath: "string",
50
50
  }
51
51
 
@@ -212,17 +212,40 @@ class Setting:
212
212
  filter=self.cmd_filter,
213
213
  )(self.toggle)
214
214
 
215
- for choice in (self.choices or self.schema.get("enum", [])) or []:
216
- add_cmd(
217
- name=f"set-{name}-{choice}",
218
- hidden=self.hidden,
219
- toggled=Condition(partial(lambda x: self.value == x, choice)),
220
- title=f"Set {self.title} to {choice}",
221
- menu_title=str(choice).replace("_", " ").capitalize(),
222
- description=f'Set the value of the "{self.name}" '
223
- f'configuration option to "{choice}"',
224
- filter=self.cmd_filter,
225
- )(partial(setattr, self, "value", choice))
215
+ schema = self.schema
216
+ if schema.get("type") == "array":
217
+ for choice in self.choices or schema.get("items", {}).get("enum") or []:
218
+ add_cmd(
219
+ name=f"toggle-{name}-{choice}",
220
+ hidden=self.hidden,
221
+ toggled=Condition(partial(lambda x: x in self.value, choice)),
222
+ title=f"Toggle {choice} into {self.title}",
223
+ menu_title=str(choice).replace("_", " ").capitalize(),
224
+ description=f'Add or remove "{choice}" to or from the list of "{self.name}"',
225
+ filter=self.cmd_filter,
226
+ )(
227
+ partial(
228
+ lambda choice: (
229
+ self.value.remove
230
+ if choice in self.value
231
+ else self.value.append
232
+ )(choice),
233
+ choice,
234
+ )
235
+ )
236
+
237
+ else:
238
+ for choice in self.choices or schema.get("enum", []) or []:
239
+ add_cmd(
240
+ name=f"set-{name}-{choice}",
241
+ hidden=self.hidden,
242
+ toggled=Condition(partial(lambda x: self.value == x, choice)),
243
+ title=f"Set {self.title} to {choice}",
244
+ menu_title=str(choice).replace("_", " ").capitalize(),
245
+ description=f'Set the value of the "{self.name}" '
246
+ f'configuration option to "{choice}"',
247
+ filter=self.cmd_filter,
248
+ )(partial(setattr, self, "value", choice))
226
249
 
227
250
  def toggle(self) -> None:
228
251
  """Toggle the setting's value."""
@@ -256,12 +279,17 @@ class Setting:
256
279
  @property
257
280
  def schema(self) -> dict[str, Any]:
258
281
  """Return a json schema property for the config item."""
259
- return {
282
+ schema = {
260
283
  "description": self.help,
261
- **({"enum": self.choices} if self.choices is not None else {}),
262
284
  **({"default": self.default} if self.default is not None else {}),
263
285
  **self._schema,
264
286
  }
287
+ if self.choices:
288
+ if self.nargs == "*" or "items" in schema:
289
+ schema["items"]["enum"] = self.choices
290
+ else:
291
+ schema["enum"] = self.choices
292
+ return schema
265
293
 
266
294
  @property
267
295
  def menu(self) -> MenuItem:
@@ -378,19 +406,14 @@ class Config:
378
406
 
379
407
  # Save a list of unknown configuration options so we can warn about them once
380
408
  # the logs are configured
381
- self.unrecognised = [
382
- (map_name, option_name)
383
- for map_name, map_values in config_maps.items()
384
- for option_name in map_values.keys() - Config.settings.keys()
385
- if not isinstance(set_values[option_name], dict)
386
- ]
387
-
388
- def warn(self) -> None:
389
- """Warn about unrecognised configuration items."""
390
- for map_name, option_name in self.unrecognised:
391
- log.warning(
392
- "Configuration option '%s' not recognised in %s", option_name, map_name
393
- )
409
+ for map_name, map_values in config_maps.items():
410
+ for option_name in map_values.keys() - Config.settings.keys():
411
+ if not isinstance(set_values[option_name], dict):
412
+ log.warning(
413
+ "Configuration option '%s' not recognised in %s",
414
+ option_name,
415
+ map_name,
416
+ )
394
417
 
395
418
  @property
396
419
  def schema(self) -> dict[str, Any]:
@@ -432,6 +455,7 @@ class Config:
432
455
  fastjsonschema.validate(self.schema, json_data)
433
456
  except fastjsonschema.JsonSchemaValueException as error:
434
457
  log.warning("Error in command line parameter `%s`: %s", name, error)
458
+ log.warning("%s: %s", name, value)
435
459
  else:
436
460
  result[name] = value
437
461
  return result
@@ -12,6 +12,7 @@ from euporie.core.convert.formats import (
12
12
  png,
13
13
  rich,
14
14
  sixel,
15
+ svg,
15
16
  )
16
17
 
17
18
  __all__ = [
@@ -26,4 +27,5 @@ __all__ = [
26
27
  "png",
27
28
  "rich",
28
29
  "sixel",
30
+ "svg",
29
31
  ]