euporie 2.8.4__py3-none-any.whl → 2.8.6__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 (131) hide show
  1. euporie/console/_commands.py +143 -0
  2. euporie/console/_settings.py +58 -0
  3. euporie/console/app.py +25 -71
  4. euporie/console/tabs/console.py +58 -62
  5. euporie/core/__init__.py +1 -1
  6. euporie/core/__main__.py +28 -11
  7. euporie/core/_settings.py +109 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +95 -0
  10. euporie/core/app/_settings.py +457 -0
  11. euporie/core/{app.py → app/app.py} +212 -576
  12. euporie/core/app/base.py +51 -0
  13. euporie/core/{current.py → app/current.py} +13 -4
  14. euporie/core/app/cursor.py +35 -0
  15. euporie/core/app/dummy.py +12 -0
  16. euporie/core/app/launch.py +28 -0
  17. euporie/core/bars/__init__.py +11 -0
  18. euporie/core/bars/command.py +205 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +20 -16
  21. euporie/core/{widgets → bars}/status.py +6 -23
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +16 -7
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +10 -20
  27. euporie/core/completion.py +3 -2
  28. euporie/core/config.py +368 -341
  29. euporie/core/convert/__init__.py +0 -30
  30. euporie/core/convert/datum.py +116 -53
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +9 -23
  33. euporie/core/convert/formats/common.py +11 -23
  34. euporie/core/convert/formats/html.py +45 -40
  35. euporie/core/convert/formats/pil.py +1 -1
  36. euporie/core/convert/formats/png.py +3 -5
  37. euporie/core/convert/formats/sixel.py +3 -3
  38. euporie/core/convert/registry.py +4 -6
  39. euporie/core/convert/utils.py +41 -4
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +98 -40
  42. euporie/core/format.py +2 -3
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +12 -21
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +386 -133
  48. euporie/core/history.py +2 -2
  49. euporie/core/inspection.py +3 -2
  50. euporie/core/io.py +207 -28
  51. euporie/core/kernel/__init__.py +1 -0
  52. euporie/core/{kernel.py → kernel/client.py} +45 -108
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +1 -8
  55. euporie/core/key_binding/bindings/basic.py +47 -7
  56. euporie/core/key_binding/bindings/completion.py +3 -8
  57. euporie/core/key_binding/bindings/micro.py +1 -6
  58. euporie/core/key_binding/bindings/mouse.py +2 -2
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/key_processor.py +43 -2
  61. euporie/core/key_binding/registry.py +2 -0
  62. euporie/core/key_binding/utils.py +22 -2
  63. euporie/core/keys.py +7156 -93
  64. euporie/core/layout/cache.py +3 -3
  65. euporie/core/layout/containers.py +48 -4
  66. euporie/core/layout/decor.py +2 -2
  67. euporie/core/layout/mouse.py +1 -1
  68. euporie/core/layout/print.py +2 -1
  69. euporie/core/layout/scroll.py +39 -34
  70. euporie/core/log.py +76 -64
  71. euporie/core/lsp.py +118 -24
  72. euporie/core/margins.py +1 -1
  73. euporie/core/path.py +62 -13
  74. euporie/core/renderer.py +58 -17
  75. euporie/core/style.py +57 -39
  76. euporie/core/suggest.py +103 -85
  77. euporie/core/tabs/__init__.py +32 -0
  78. euporie/core/tabs/_settings.py +113 -0
  79. euporie/core/tabs/base.py +80 -470
  80. euporie/core/tabs/kernel.py +419 -0
  81. euporie/core/tabs/notebook.py +24 -101
  82. euporie/core/utils.py +92 -15
  83. euporie/core/validation.py +1 -1
  84. euporie/core/widgets/_settings.py +188 -0
  85. euporie/core/widgets/cell.py +19 -50
  86. euporie/core/widgets/cell_outputs.py +25 -36
  87. euporie/core/widgets/decor.py +11 -41
  88. euporie/core/widgets/dialog.py +62 -27
  89. euporie/core/widgets/display.py +12 -15
  90. euporie/core/widgets/file_browser.py +2 -23
  91. euporie/core/widgets/forms.py +8 -5
  92. euporie/core/widgets/inputs.py +13 -70
  93. euporie/core/widgets/layout.py +2 -1
  94. euporie/core/widgets/logo.py +49 -0
  95. euporie/core/widgets/menu.py +10 -8
  96. euporie/core/widgets/pager.py +6 -10
  97. euporie/core/widgets/palette.py +6 -6
  98. euporie/hub/app.py +52 -35
  99. euporie/notebook/_commands.py +24 -0
  100. euporie/notebook/_settings.py +107 -0
  101. euporie/notebook/app.py +49 -171
  102. euporie/notebook/filters.py +1 -1
  103. euporie/notebook/tabs/__init__.py +46 -7
  104. euporie/notebook/tabs/_commands.py +714 -0
  105. euporie/notebook/tabs/_settings.py +32 -0
  106. euporie/notebook/tabs/display.py +4 -4
  107. euporie/notebook/tabs/edit.py +11 -44
  108. euporie/notebook/tabs/json.py +5 -5
  109. euporie/notebook/tabs/log.py +1 -18
  110. euporie/notebook/tabs/notebook.py +11 -660
  111. euporie/notebook/widgets/_commands.py +11 -0
  112. euporie/notebook/widgets/_settings.py +19 -0
  113. euporie/notebook/widgets/side_bar.py +14 -34
  114. euporie/preview/_settings.py +104 -0
  115. euporie/preview/app.py +6 -31
  116. euporie/preview/tabs/notebook.py +6 -72
  117. euporie/web/__init__.py +1 -0
  118. euporie/web/tabs/__init__.py +14 -0
  119. euporie/web/tabs/web.py +11 -6
  120. euporie/web/widgets/__init__.py +1 -0
  121. euporie/web/widgets/webview.py +5 -15
  122. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/METADATA +10 -8
  123. euporie-2.8.6.dist-info/RECORD +175 -0
  124. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/WHEEL +1 -1
  125. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +2 -2
  126. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +1 -1
  127. euporie/core/launch.py +0 -64
  128. euporie/core/terminal.py +0 -522
  129. euporie-2.8.4.dist-info/RECORD +0 -147
  130. {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
  131. {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from functools import lru_cache
6
+ from functools import cache
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  from prompt_toolkit.data_structures import Point
@@ -17,7 +17,7 @@ from prompt_toolkit.layout.layout import walk
17
17
  from prompt_toolkit.layout.mouse_handlers import MouseHandlers
18
18
  from prompt_toolkit.mouse_events import MouseEvent
19
19
 
20
- from euporie.core.current import get_app
20
+ from euporie.core.app.current import get_app
21
21
  from euporie.core.data_structures import DiInt
22
22
  from euporie.core.layout.screen import BoundedWritePosition, Screen
23
23
 
@@ -313,7 +313,7 @@ class CachedContainer(Container):
313
313
  screen.visible_windows_to_write_positions.update(new_wps)
314
314
  screen.height = max(screen.height, self.screen.height)
315
315
 
316
- @lru_cache(maxsize=None)
316
+ @cache
317
317
  def _wrap_mouse_handler(handler: Callable) -> MouseHandler:
318
318
  def _wrapped(mouse_event: MouseEvent) -> NotImplementedOrNone:
319
319
  # Modify mouse events to reflect position of content
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from functools import partial
6
+ from functools import lru_cache, partial
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  from prompt_toolkit.application.current import get_app
@@ -35,7 +35,8 @@ from euporie.core.layout.controls import DummyControl
35
35
  from euporie.core.layout.screen import BoundedWritePosition
36
36
 
37
37
  if TYPE_CHECKING:
38
- from typing import Any, Callable, Sequence
38
+ from collections.abc import Sequence
39
+ from typing import Any, Callable
39
40
 
40
41
  from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples
41
42
  from prompt_toolkit.key_binding.key_bindings import (
@@ -55,6 +56,42 @@ if TYPE_CHECKING:
55
56
  log = logging.getLogger(__name__)
56
57
 
57
58
 
59
+ @lru_cache(maxsize=None)
60
+ class DummyContainer(Container):
61
+ """Base class for user interface layout."""
62
+
63
+ def __init__(self, width: int = 0, height: int = 0) -> None:
64
+ """Define width and height if any."""
65
+ self.width = width
66
+ self.height = height
67
+
68
+ def reset(self) -> None:
69
+ """Reset the state of this container (does nothing)."""
70
+
71
+ def preferred_width(self, max_available_width: int) -> Dimension:
72
+ """Return a zero-width dimension."""
73
+ return Dimension.exact(self.width)
74
+
75
+ def preferred_height(self, width: int, max_available_height: int) -> Dimension:
76
+ """Return a zero-height dimension."""
77
+ return Dimension.exact(self.height)
78
+
79
+ def write_to_screen(
80
+ self,
81
+ screen: Screen,
82
+ mouse_handlers: MouseHandlers,
83
+ write_position: WritePosition,
84
+ parent_style: str,
85
+ erase_bg: bool,
86
+ z_index: int | None,
87
+ ) -> None:
88
+ """Write the actual content to the screen. Does nothing."""
89
+
90
+ def get_children(self) -> list[Container]:
91
+ """Return an empty list of child :class:`.Container` objects."""
92
+ return []
93
+
94
+
58
95
  class HSplit(ptk_containers.HSplit):
59
96
  """Several layouts, one stacked above/under the other."""
60
97
 
@@ -76,6 +113,8 @@ class HSplit(ptk_containers.HSplit):
76
113
  style: str | Callable[[], str] = "",
77
114
  ) -> None:
78
115
  """Initialize the HSplit with a cache."""
116
+ if window_too_small is None:
117
+ window_too_small = DummyContainer()
79
118
  super().__init__(
80
119
  children=children,
81
120
  window_too_small=window_too_small,
@@ -244,6 +283,8 @@ class VSplit(ptk_containers.VSplit):
244
283
  style: str | Callable[[], str] = "",
245
284
  ) -> None:
246
285
  """Initialize the VSplit with a cache."""
286
+ if window_too_small is None:
287
+ window_too_small = DummyContainer()
247
288
  super().__init__(
248
289
  children=children,
249
290
  window_too_small=window_too_small,
@@ -423,17 +464,20 @@ class Window(ptk_containers.Window):
423
464
  z_index: int | None,
424
465
  ) -> None:
425
466
  """Write window to screen."""
426
- assert isinstance(write_position, BoundedWritePosition)
427
467
  # If dont_extend_width/height was given, then reduce width/height in
428
468
  # WritePosition, if the parent wanted us to paint in a bigger area.
429
469
  # (This happens if this window is bundled with another window in a
430
470
  # HSplit/VSplit, but with different size requirements.)
471
+ if isinstance(write_position, BoundedWritePosition):
472
+ bbox = write_position.bbox
473
+ else:
474
+ bbox = DiInt(0, 0, 0, 0)
431
475
  write_position = BoundedWritePosition(
432
476
  xpos=write_position.xpos,
433
477
  ypos=write_position.ypos,
434
478
  width=write_position.width,
435
479
  height=write_position.height,
436
- bbox=write_position.bbox,
480
+ bbox=bbox,
437
481
  )
438
482
 
439
483
  if self.dont_extend_width():
@@ -15,8 +15,8 @@ from prompt_toolkit.layout.dimension import Dimension
15
15
  from prompt_toolkit.layout.screen import Char, Screen, WritePosition
16
16
  from prompt_toolkit.mouse_events import MouseEventType
17
17
 
18
+ from euporie.core.app.current import get_app
18
19
  from euporie.core.border import ThinLine
19
- from euporie.core.current import get_app
20
20
  from euporie.core.style import ColorPaletteColor
21
21
 
22
22
  if TYPE_CHECKING:
@@ -341,7 +341,7 @@ class DropShadow(Container):
341
341
  ) -> None:
342
342
  """Draw the wrapped container with the additional style."""
343
343
  attr_cache = self.renderer._attrs_for_style
344
- if attr_cache is not None:
344
+ if attr_cache is not None and self.amount:
345
345
  ypos = write_position.ypos
346
346
  xpos = write_position.xpos
347
347
  amount = self.amount
@@ -13,7 +13,7 @@ from prompt_toolkit.layout.containers import (
13
13
  )
14
14
  from prompt_toolkit.mouse_events import MouseEventType
15
15
 
16
- from euporie.core.current import get_app
16
+ from euporie.core.app.current import get_app
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from typing import Callable
@@ -16,7 +16,8 @@ from prompt_toolkit.layout.dimension import Dimension, to_dimension
16
16
  from euporie.core.layout.screen import BoundedWritePosition
17
17
 
18
18
  if TYPE_CHECKING:
19
- from typing import Callable, Sequence
19
+ from collections.abc import Sequence
20
+ from typing import Callable
20
21
 
21
22
  from prompt_toolkit.key_binding.key_bindings import (
22
23
  KeyBindingsBase,
@@ -1,7 +1,8 @@
1
- """Contains containers which display children at full height vertially stacked."""
1
+ """Contains containers which display children at full height vertically stacked."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import logging
6
7
  from typing import TYPE_CHECKING, cast
7
8
 
@@ -20,10 +21,10 @@ from prompt_toolkit.mouse_events import MouseEvent, MouseEventType, MouseModifie
20
21
 
21
22
  from euporie.core.layout.cache import CachedContainer
22
23
  from euporie.core.layout.screen import BoundedWritePosition
23
- from euporie.core.utils import run_in_thread_with_context
24
24
 
25
25
  if TYPE_CHECKING:
26
- from typing import Callable, Literal, Sequence
26
+ from collections.abc import Sequence
27
+ from typing import Callable, Literal
27
28
 
28
29
  from prompt_toolkit.key_binding.key_bindings import (
29
30
  KeyBindingsBase,
@@ -43,6 +44,8 @@ log = logging.getLogger(__name__)
43
44
  class ScrollingContainer(Container):
44
45
  """A scrollable container which renders only the currently visible children."""
45
46
 
47
+ render_info: WindowRenderInfo | None
48
+
46
49
  def __init__(
47
50
  self,
48
51
  children: Callable[[], Sequence[AnyContainer]] | Sequence[AnyContainer],
@@ -65,7 +68,7 @@ class ScrollingContainer(Container):
65
68
  self._child_cache: dict[int, CachedContainer] = {}
66
69
  self._children: list[CachedContainer] = []
67
70
  self.refresh_children = True
68
- self.pre_rendered = 0.0
71
+ self.pre_rendered: float | None = None
69
72
 
70
73
  self._selected_slice = slice(
71
74
  0, 1
@@ -89,23 +92,33 @@ class ScrollingContainer(Container):
89
92
 
90
93
  def pre_render_children(self, width: int, height: int) -> None:
91
94
  """Render all unrendered children in a background thread."""
95
+ self.pre_rendered = 0.0
96
+ children = self.all_children()
97
+ incr = 1 / len(children)
98
+ app = get_app()
99
+
100
+ def _cb(task: asyncio.Task) -> None:
101
+ """Task callback to update pre-rendering percentage."""
102
+ assert isinstance(self.pre_rendered, float)
103
+ self.pre_rendered += incr
104
+ app.invalidate()
92
105
 
93
- def _render_in_thread() -> None:
94
- """Render children in thread."""
95
- children = self.all_children()
96
- n_children = len(children)
97
- app = get_app()
98
- for i, child in enumerate(children):
99
- if not app._is_running:
100
- return
101
- if isinstance(child, CachedContainer):
102
- child.render(width, height)
103
- self.pre_rendered = i / n_children
104
- app.invalidate()
106
+ tasks = set()
107
+ for child in children:
108
+ if isinstance(child, CachedContainer):
109
+ task = app.create_background_task(
110
+ asyncio.to_thread(child.render, width, height)
111
+ )
112
+ task.add_done_callback(_cb)
113
+ tasks.add(task)
114
+
115
+ async def _finish() -> None:
116
+ await asyncio.gather(*tasks)
105
117
  self.pre_rendered = 1.0
106
118
  app.invalidate()
119
+ # app.exit()
107
120
 
108
- run_in_thread_with_context(_render_in_thread)
121
+ app.create_background_task(_finish())
109
122
 
110
123
  def reset(self) -> None:
111
124
  """Reset the state of this container and all the children."""
@@ -234,7 +247,6 @@ class ScrollingContainer(Container):
234
247
  :py:const:`None`
235
248
 
236
249
  """
237
- # self.refresh_children = True
238
250
  if n > 0:
239
251
  if (
240
252
  min(self.visible_indices) == 0
@@ -252,8 +264,8 @@ class ScrollingContainer(Container):
252
264
  if bottom_pos is not None:
253
265
  n = max(
254
266
  n,
255
- self.last_write_position.height
256
- - (bottom_pos + bottom_child.height + self.scrolling),
267
+ (bottom_pos + bottom_child.height + self.scrolling)
268
+ - self.last_write_position.height,
257
269
  )
258
270
  if (
259
271
  bottom_pos + bottom_child.height + self.scrolling + n
@@ -362,9 +374,6 @@ class ScrollingContainer(Container):
362
374
  # Record children which are currently visible
363
375
  visible_indices = set()
364
376
 
365
- # Ensure we have the right children
366
- all_children = self.all_children()
367
-
368
377
  # Force the selected children to refresh
369
378
  selected_indices = self.selected_indices
370
379
  self._selected_children: list[CachedContainer] = []
@@ -546,19 +555,10 @@ class ScrollingContainer(Container):
546
555
  # are partially obscured
547
556
  self.last_write_position = write_position
548
557
 
549
- # Calculate scrollbar info
550
- sizes = self.known_sizes
551
- avg_size = sum(sizes.values()) / len(sizes) if sizes else 0
552
- n_children = len(all_children)
553
- for i in range(n_children):
554
- if i not in sizes:
555
- sizes[i] = int(avg_size)
556
- content_height = max(sum(sizes.values()), 1)
557
-
558
558
  # Mock up a WindowRenderInfo so we can draw a scrollbar margin
559
559
  self.render_info = WindowRenderInfo(
560
560
  window=cast("Window", self),
561
- ui_content=UIContent(line_count=content_height),
561
+ ui_content=UIContent(line_count=max(sum(self.known_sizes.values()), 1)),
562
562
  horizontal_scroll=0,
563
563
  vertical_scroll=self.vertical_scroll,
564
564
  window_width=available_width,
@@ -574,7 +574,7 @@ class ScrollingContainer(Container):
574
574
  self.scrolling = 0
575
575
 
576
576
  # Trigger pre-rendering of children
577
- if not self.pre_rendered:
577
+ if self.pre_rendered is None:
578
578
  self.pre_render_children(available_width, available_height)
579
579
 
580
580
  @property
@@ -727,9 +727,14 @@ class ScrollingContainer(Container):
727
727
  def known_sizes(self) -> dict[int, int]:
728
728
  """A dictionary mapping child indices to height values."""
729
729
  sizes = {}
730
+ missing = set()
730
731
  for i, child in enumerate(self._children):
731
732
  if isinstance(child, CachedContainer) and child.height:
732
733
  sizes[i] = child.height
734
+ else:
735
+ missing.add(i)
736
+ avg = int(sum(sizes.values()) / len(sizes))
737
+ sizes.update(dict.fromkeys(missing, avg))
733
738
  return sizes
734
739
 
735
740
  def _scroll_up(self) -> None:
euporie/core/log.py CHANGED
@@ -20,12 +20,10 @@ from prompt_toolkit.renderer import (
20
20
  from prompt_toolkit.shortcuts.utils import print_formatted_text
21
21
  from prompt_toolkit.styles.pygments import style_from_pygments_cls
22
22
  from prompt_toolkit.styles.style import Style, merge_styles
23
- from pygments.styles import get_style_by_name
24
23
 
25
- from euporie.core.config import add_setting
26
24
  from euporie.core.ft.utils import indent, lex, wrap
27
25
  from euporie.core.io import PseudoTTY
28
- from euporie.core.style import LOG_STYLE
26
+ from euporie.core.style import LOG_STYLE, get_style_by_name
29
27
  from euporie.core.utils import dict_merge
30
28
 
31
29
  if TYPE_CHECKING:
@@ -42,6 +40,65 @@ log = logging.getLogger(__name__)
42
40
  LOG_QUEUE: deque = deque(maxlen=1000)
43
41
 
44
42
 
43
+ class BufferedLogs(logging.Handler):
44
+ """A handler that collects log records and replays them on exit."""
45
+
46
+ def __init__(self, logger: logging.Logger | None = None) -> None:
47
+ """Initialize the collector.
48
+
49
+ Args:
50
+ logger: Logger to collect from and replay to. If None, uses root logger.
51
+ """
52
+ super().__init__()
53
+ self.records: list[logging.LogRecord] = []
54
+ self._logger = logger or logging.getLogger()
55
+ self._original_handlers: list[logging.Handler] = []
56
+
57
+ def emit(self, record: logging.LogRecord) -> None:
58
+ """Store the log record."""
59
+ self.records.append(record)
60
+
61
+ def replay(self) -> None:
62
+ """Replay collected logs through the original logger."""
63
+ for record in self.records:
64
+ if record.exc_info:
65
+ # Create a new record to avoid issues with stale exc_info
66
+ record = logging.LogRecord(
67
+ record.name,
68
+ record.levelno,
69
+ record.pathname,
70
+ record.lineno,
71
+ record.msg,
72
+ record.args,
73
+ record.exc_info,
74
+ record.funcName,
75
+ )
76
+ self._logger.handle(record)
77
+
78
+ def __enter__(self) -> BufferedLogs:
79
+ """Store and replace the log handlers."""
80
+ # Save and remove existing handlers
81
+ self._original_handlers = self._logger.handlers[:]
82
+ self._logger.handlers.clear()
83
+ # Add ourselves as the only handler
84
+ self._logger.addHandler(self)
85
+ return self
86
+
87
+ def __exit__(
88
+ self,
89
+ exc_type: type[BaseException] | None,
90
+ exc_value: BaseException | None,
91
+ exc_traceback: TracebackType | None,
92
+ ) -> None:
93
+ """Restore the original handlers."""
94
+ # Remove ourselves
95
+ self._logger.removeHandler(self)
96
+ # Restore original handlers
97
+ self._logger.handlers = self._original_handlers
98
+ # Replay collected records through original handlers
99
+ self.replay()
100
+
101
+
45
102
  class FtFormatter(logging.Formatter):
46
103
  """Base class for formatted text logging formatter."""
47
104
 
@@ -240,12 +297,17 @@ class StdoutFormatter(FtFormatter):
240
297
  msg_pad = len(date) + 10
241
298
  msg_pad_1st_line = msg_pad + 1 + len(ref)
242
299
 
243
- msg_lines = textwrap.wrap(
244
- record.message,
245
- width=width,
246
- initial_indent=" " * msg_pad_1st_line,
247
- subsequent_indent=" " * msg_pad,
248
- )
300
+ msg_lines = "\n".join(
301
+ textwrap.wrap(
302
+ record.message,
303
+ width=width,
304
+ initial_indent=" " * msg_pad_1st_line,
305
+ replace_whitespace=False,
306
+ )
307
+ ).split("\n")
308
+ subsequent_indent = " " * msg_pad
309
+ for i in range(1, len(msg_lines)):
310
+ msg_lines[i] = f"{subsequent_indent}{msg_lines[i]}"
249
311
 
250
312
  output: StyleAndTextTuples = [
251
313
  ("class:pygments.literal.date", date),
@@ -416,7 +478,7 @@ def setup_logs(config: Config | None = None) -> None:
416
478
  }
417
479
 
418
480
  if config is not None:
419
- log_file = config.get("log_file", "")
481
+ log_file = config.log_file or ""
420
482
  log_file_is_stdout = log_file in {"-", "/dev/stdout"}
421
483
  log_level = config.log_level.upper()
422
484
 
@@ -433,25 +495,18 @@ def setup_logs(config: Config | None = None) -> None:
433
495
  # Configure stdout handler
434
496
  if log_file_is_stdout:
435
497
  stdout_level = log_level
436
- elif (app_cls := config.app_cls) is not None and (
437
- log_stdout_level := app_cls.log_stdout_level
438
- ):
439
- stdout_level = log_stdout_level.upper()
440
498
  else:
441
- stdout_level = "CRITICAL"
499
+ stdout_level = config.log_level_stdout.upper()
442
500
  log_config["handlers"]["stdout"]["level"] = stdout_level
443
- if syntax_theme := config.get("syntax_theme"):
501
+ if syntax_theme := config.syntax_theme:
444
502
  log_config["handlers"]["stdout"]["pygments_theme"] = syntax_theme
445
503
 
446
504
  # Configure euporie logger
447
- log_config["loggers"]["euporie"]["level"] = config.log_level.upper()
505
+ log_config["loggers"]["euporie"]["level"] = log_level
448
506
 
449
507
  # Update log_config based on additional config dict provided
450
508
  if config.log_config:
451
- import json
452
-
453
- extra_config = json.loads(config.log_config)
454
- dict_merge(log_config, extra_config)
509
+ dict_merge(log_config, config.log_config)
455
510
 
456
511
  # Configure the logger
457
512
  # Pytype used TypedDicts to validate the dictionary structure, but I cannot get
@@ -463,46 +518,3 @@ def setup_logs(config: Config | None = None) -> None:
463
518
 
464
519
  # Log uncaught exceptions
465
520
  sys.excepthook = handle_exception
466
-
467
-
468
- # ################################### Settings ########################################
469
-
470
-
471
- add_setting(
472
- name="log_file",
473
- flags=["--log-file"],
474
- nargs="?",
475
- default="",
476
- type_=str,
477
- title="the log file path",
478
- help_="File path for logs",
479
- description="""
480
- When set to a file path, the log output will be written to the given path.
481
- If no value is given output will be sent to the standard output.
482
- """,
483
- )
484
-
485
- add_setting(
486
- name="log_level",
487
- flags=["--log-level"],
488
- type_=str,
489
- default="warning",
490
- title="the log level",
491
- help_="Set the log level",
492
- choices=["debug", "info", "warning", "error", "critical"],
493
- description="""
494
- When set, logging events at the given level are emitted.
495
- """,
496
- )
497
-
498
- add_setting(
499
- name="log_config",
500
- flags=["--log-config"],
501
- type_=str,
502
- default=None,
503
- title="additional logging configuration",
504
- help_="Additional logging configuration",
505
- description="""
506
- A JSON string specifying additional logging configuration.
507
- """,
508
- )