euporie 2.8.6__py3-none-any.whl → 2.8.7__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 (64) hide show
  1. euporie/console/app.py +2 -0
  2. euporie/console/tabs/console.py +27 -17
  3. euporie/core/__init__.py +2 -2
  4. euporie/core/app/_commands.py +4 -21
  5. euporie/core/app/app.py +13 -7
  6. euporie/core/bars/command.py +9 -6
  7. euporie/core/bars/search.py +43 -2
  8. euporie/core/border.py +7 -2
  9. euporie/core/comm/base.py +2 -2
  10. euporie/core/comm/ipywidgets.py +3 -3
  11. euporie/core/commands.py +44 -8
  12. euporie/core/completion.py +14 -6
  13. euporie/core/convert/datum.py +7 -7
  14. euporie/core/data_structures.py +20 -1
  15. euporie/core/filters.py +8 -0
  16. euporie/core/ft/html.py +47 -40
  17. euporie/core/graphics.py +11 -3
  18. euporie/core/history.py +15 -5
  19. euporie/core/inspection.py +16 -9
  20. euporie/core/kernel/__init__.py +53 -1
  21. euporie/core/kernel/base.py +571 -0
  22. euporie/core/kernel/{client.py → jupyter.py} +173 -430
  23. euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
  24. euporie/core/kernel/local.py +694 -0
  25. euporie/core/key_binding/bindings/basic.py +6 -3
  26. euporie/core/keys.py +26 -25
  27. euporie/core/layout/cache.py +31 -7
  28. euporie/core/layout/containers.py +88 -13
  29. euporie/core/layout/scroll.py +45 -148
  30. euporie/core/log.py +1 -1
  31. euporie/core/style.py +2 -1
  32. euporie/core/suggest.py +155 -74
  33. euporie/core/tabs/__init__.py +10 -0
  34. euporie/core/tabs/_commands.py +76 -0
  35. euporie/core/tabs/_settings.py +16 -0
  36. euporie/core/tabs/base.py +22 -8
  37. euporie/core/tabs/kernel.py +81 -35
  38. euporie/core/tabs/notebook.py +14 -22
  39. euporie/core/utils.py +1 -1
  40. euporie/core/validation.py +8 -8
  41. euporie/core/widgets/_settings.py +19 -2
  42. euporie/core/widgets/cell.py +31 -31
  43. euporie/core/widgets/cell_outputs.py +10 -1
  44. euporie/core/widgets/dialog.py +30 -75
  45. euporie/core/widgets/forms.py +71 -59
  46. euporie/core/widgets/inputs.py +7 -4
  47. euporie/core/widgets/layout.py +281 -93
  48. euporie/core/widgets/menu.py +55 -15
  49. euporie/core/widgets/palette.py +3 -1
  50. euporie/core/widgets/tree.py +86 -76
  51. euporie/notebook/app.py +35 -16
  52. euporie/notebook/tabs/edit.py +4 -4
  53. euporie/notebook/tabs/json.py +6 -2
  54. euporie/notebook/tabs/notebook.py +26 -8
  55. euporie/preview/tabs/notebook.py +17 -13
  56. euporie/web/tabs/web.py +22 -3
  57. euporie/web/widgets/webview.py +3 -0
  58. {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/METADATA +1 -1
  59. {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/RECORD +64 -61
  60. {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/entry_points.txt +1 -1
  61. {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/licenses/LICENSE +1 -1
  62. {euporie-2.8.6.data → euporie-2.8.7.data}/data/share/applications/euporie-console.desktop +0 -0
  63. {euporie-2.8.6.data → euporie-2.8.7.data}/data/share/applications/euporie-notebook.desktop +0 -0
  64. {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/WHEEL +0 -0
@@ -6,7 +6,7 @@ import asyncio
6
6
  import logging
7
7
  import os
8
8
  import weakref
9
- from functools import partial
9
+ from functools import lru_cache, partial
10
10
  from pathlib import Path
11
11
  from typing import TYPE_CHECKING, cast
12
12
  from weakref import WeakKeyDictionary
@@ -50,6 +50,7 @@ if TYPE_CHECKING:
50
50
  from prompt_toolkit.completion.base import Completer
51
51
  from prompt_toolkit.formatted_text.base import StyleAndTextTuples
52
52
 
53
+ from euporie.core.border import GridStyle
53
54
  from euporie.core.format import Formatter
54
55
  from euporie.core.inspection import Inspector
55
56
  from euporie.core.lsp import LspClient
@@ -79,6 +80,18 @@ def get_cell_id(cell_json: dict) -> str:
79
80
  return cell_id
80
81
 
81
82
 
83
+ @lru_cache(maxsize=32)
84
+ def _get_border_style(
85
+ selected: bool, focused: bool, show_borders: bool, multi_selected: bool
86
+ ) -> GridStyle:
87
+ """Get the border style grid based on cell state."""
88
+ if not (show_borders or selected):
89
+ return NoLine.grid
90
+ if focused and multi_selected:
91
+ return ThickLine.outer
92
+ return ThinLine.outer
93
+
94
+
82
95
  class Cell:
83
96
  """A kernel_tab cell element.
84
97
 
@@ -86,8 +99,6 @@ class Cell:
86
99
  focused.
87
100
  """
88
101
 
89
- input_box: KernelInput
90
-
91
102
  def __init__(
92
103
  self, index: int, json: dict, kernel_tab: BaseNotebook, is_new: bool = False
93
104
  ) -> None:
@@ -158,6 +169,14 @@ class Cell:
158
169
  """Update cell json when the input buffer has been edited."""
159
170
  weak_self._set_input(buf.text)
160
171
  weak_self.kernel_tab.dirty = True
172
+ weak_self.on_change()
173
+ # Re-render markdown cells when edited outside of edit mode
174
+ if (
175
+ weak_self.cell_type == "markdown"
176
+ and not weak_self.kernel_tab.in_edit_mode()
177
+ ):
178
+ weak_self.output_area.json = weak_self.output_json
179
+ weak_self.refresh()
161
180
 
162
181
  def on_cursor_position_changed(buf: Buffer) -> None:
163
182
  """Respond to cursor movements."""
@@ -199,42 +218,22 @@ class Cell:
199
218
  )
200
219
  self.input_box.buffer.name = self.cell_type
201
220
 
202
- self.input_box.buffer.on_text_changed += lambda buf: weak_self.on_change()
203
-
204
221
  def border_char(name: str) -> Callable[..., str]:
205
222
  """Return a function which returns the cell border character to display."""
206
223
 
207
224
  def _inner() -> str:
208
- grid = NoLine.grid
209
- if weak_self and (
210
- weak_self.kernel_tab.app.config.show_cell_borders
211
- or weak_self.selected
212
- ):
213
- if weak_self.focused and multiple_cells_selected():
214
- grid = ThickLine.outer
215
- else:
216
- grid = ThinLine.outer
225
+ if not weak_self:
226
+ return " "
227
+ grid = _get_border_style(
228
+ weak_self.selected,
229
+ weak_self.focused,
230
+ weak_self.kernel_tab.app.config.show_cell_borders,
231
+ multiple_cells_selected(),
232
+ )
217
233
  return getattr(grid, name.upper())
218
234
 
219
235
  return _inner
220
236
 
221
- # @lru_cache(maxsize=None)
222
- # def _cell_border_char(
223
- # name: str,
224
- # show_cell_borders: bool,
225
- # focused: bool,
226
- # selected: bool,
227
- # multiple_cells_selected: bool,
228
- # ) -> str:
229
- # if show_cell_borders or selected:
230
- # if focused and multiple_cells_selected:
231
- # grid = ThickLine.outer
232
- # else:
233
- # grid = ThinLine.outer
234
- # else:
235
- # grid = NoLine.grid
236
- # return getattr(grid, name.upper())
237
-
238
237
  self.control = Window(
239
238
  FormattedTextControl(
240
239
  border_char("TOP_LEFT"),
@@ -764,6 +763,7 @@ class Cell:
764
763
  def set_execution_count(self, n: int) -> None:
765
764
  """Set the execution count of the cell."""
766
765
  self.json["execution_count"] = n
766
+ self.refresh()
767
767
 
768
768
  def add_output(self, output_json: dict[str, Any], own: bool) -> None:
769
769
  """Add a new output to the cell."""
@@ -116,6 +116,16 @@ class CellOutputDataElement(CellOutputElement):
116
116
  format_ = data_format
117
117
  break
118
118
 
119
+ config = get_app().config
120
+
121
+ # Limit size of text only outputs
122
+ if (
123
+ format_ == "ansi"
124
+ and (limit := config.text_output_limit) > 0
125
+ and len(data) > limit
126
+ ):
127
+ data = data[:limit] + "\n… (Output truncated)"
128
+
119
129
  self._datum = Datum(
120
130
  data,
121
131
  format_,
@@ -123,7 +133,6 @@ class CellOutputDataElement(CellOutputElement):
123
133
  py=metadata.get("height"),
124
134
  bg=bg_color,
125
135
  )
126
- config = get_app().config
127
136
 
128
137
  self.container = Display(
129
138
  self._datum,
@@ -16,6 +16,7 @@ from prompt_toolkit.filters import (
16
16
  buffer_has_focus,
17
17
  has_completions,
18
18
  has_focus,
19
+ vi_insert_mode,
19
20
  )
20
21
  from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text
21
22
  from prompt_toolkit.formatted_text.utils import split_lines
@@ -40,7 +41,6 @@ from euporie.core.border import (
40
41
  UpperRightHalfLine,
41
42
  )
42
43
  from euporie.core.commands import add_cmd
43
- from euporie.core.filters import tab_can_save
44
44
  from euporie.core.ft.utils import FormattedTextAlign, align, lex
45
45
  from euporie.core.key_binding.registry import register_bindings
46
46
  from euporie.core.layout.containers import HSplit, VSplit, Window
@@ -64,6 +64,7 @@ if TYPE_CHECKING:
64
64
  from prompt_toolkit.mouse_events import MouseEvent
65
65
 
66
66
  from euporie.core.app.app import BaseApp
67
+ from euporie.core.kernel.base import KernelInfo
67
68
  from euporie.core.tabs.base import Tab
68
69
  from euporie.core.tabs.kernel import KernelTab
69
70
 
@@ -197,7 +198,7 @@ class Dialog(Float, metaclass=ABCMeta):
197
198
 
198
199
  # Create key-bindings
199
200
  kb = KeyBindings()
200
- kb.add("escape")(lambda event: self.hide())
201
+ kb.add("escape", filter=~(buffer_has_focus & vi_insert_mode))(self.hide)
201
202
  kb.add("tab", filter=~has_completions)(focus_next)
202
203
  kb.add("s-tab", filter=~has_completions)(focus_previous)
203
204
 
@@ -597,21 +598,6 @@ class SaveAsDialog(FileDialog):
597
598
  if callable(cb):
598
599
  cb()
599
600
 
600
- # ################################### Commands ####################################
601
-
602
- @staticmethod
603
- @add_cmd(
604
- menu_title="Save As…",
605
- filter=tab_can_save,
606
- )
607
- def _save_as() -> None:
608
- """Save the current file at a new location."""
609
- from euporie.core.app.current import get_app
610
-
611
- app = get_app()
612
- if dialog := app.dialogs.get("save-as"):
613
- dialog.show(tab=app.tab)
614
-
615
601
  # ################################# Key Bindings ##################################
616
602
 
617
603
  register_bindings(
@@ -655,62 +641,37 @@ class SelectKernelDialog(Dialog):
655
641
 
656
642
  title = "Select Kernel"
657
643
 
658
- def load(
659
- self,
660
- kernel_specs: dict[str, Any] | None = None,
661
- runtime_dirs: dict[str, Path] | None = None,
662
- tab: KernelTab | None = None,
663
- message: str = "",
664
- ) -> None:
644
+ def load(self, tab: KernelTab | None = None, message: str = "") -> None:
665
645
  """Load dialog body & buttons."""
666
- from jupyter_core.paths import jupyter_runtime_dir
667
-
646
+ from euporie.core.kernel import list_kernels
668
647
  from euporie.core.widgets.layout import TabbedSplit
669
648
 
670
- kernel_specs = kernel_specs or {}
671
- runtime_dirs = runtime_dirs or {}
672
-
673
- options_specs = Select(
674
- options=list(kernel_specs.keys()),
675
- labels=[
676
- kernel_spec.get("spec", {}).get("display_name", kernel_name)
677
- for kernel_name, kernel_spec in kernel_specs.items()
678
- ],
679
- style="class:input,radio-buttons",
680
- prefix=("○", "◉"),
681
- multiple=False,
682
- border=None,
683
- rows=5,
684
- dont_extend_width=False,
685
- )
686
- self.to_focus = options_specs
687
-
688
- connection_files = {
689
- path.name: path
690
- for path in Path(jupyter_runtime_dir()).glob("kernel-*.json")
691
- }
692
- options_files = Select(
693
- options=list(connection_files.values()),
694
- labels=list(connection_files.keys()),
695
- style="class:input,radio-buttons",
696
- prefix=("○", "◉"),
697
- multiple=False,
698
- border=None,
699
- rows=5,
700
- dont_extend_width=False,
701
- )
702
-
703
- msg_ft = (f"{message}\n\n" if message else "") + "Please select a kernel:"
649
+ infos = list_kernels()
650
+ infos_by_kind: dict[str, list[KernelInfo]] = {}
651
+ for info in infos:
652
+ infos_by_kind.setdefault(info.kind, []).append(info)
653
+
654
+ selects = {}
655
+ for kind, infos in infos_by_kind.items():
656
+ selects[kind] = Select(
657
+ options=infos,
658
+ labels=[info.display_name for info in infos],
659
+ style="class:input,radio-buttons",
660
+ prefix=("○", "◉"),
661
+ multiple=False,
662
+ border=None,
663
+ rows=5,
664
+ dont_extend_width=False,
665
+ )
704
666
 
705
667
  self.body = HSplit(
706
668
  [
707
- Label(msg_ft),
669
+ Label(
670
+ (f"{message}\n\n" if message else "") + "Please select a kernel:"
671
+ ),
708
672
  tabs := TabbedSplit(
709
- [
710
- FocusedStyle(options_specs),
711
- FocusedStyle(options_files),
712
- ],
713
- titles=["New", "Existing"],
673
+ [FocusedStyle(select) for select in selects.values()],
674
+ titles=[kind.title() for kind in selects],
714
675
  width=Dimension(min=30),
715
676
  ),
716
677
  ]
@@ -719,15 +680,9 @@ class SelectKernelDialog(Dialog):
719
680
  def _change_kernel() -> None:
720
681
  self.hide()
721
682
  assert tab is not None
722
- if tabs.active == 0:
723
- name = options_specs.value
724
- connection_file = None
725
- else:
726
- name = None
727
- connection_file = options_files.value
728
- tab.kernel.change(
729
- name=name, connection_file=connection_file, cb=tab.kernel_started
730
- )
683
+ if (index := tabs.active) is not None:
684
+ info = list(selects.values())[index].value
685
+ tab.switch_kernel(info.factory)
731
686
 
732
687
  self.buttons = {
733
688
  "Select": _change_kernel,
@@ -11,17 +11,19 @@ from math import ceil, floor
11
11
  from typing import TYPE_CHECKING, cast
12
12
  from weakref import finalize
13
13
 
14
- from prompt_toolkit.buffer import ValidationState
14
+ from prompt_toolkit.auto_suggest import DynamicAutoSuggest
15
+ from prompt_toolkit.buffer import Buffer, ValidationState
15
16
  from prompt_toolkit.cache import SimpleCache
16
- from prompt_toolkit.completion.base import ConditionalCompleter
17
- from prompt_toolkit.completion.word_completer import WordCompleter
17
+ from prompt_toolkit.completion import Completer, ConditionalCompleter, WordCompleter
18
18
  from prompt_toolkit.data_structures import Point
19
+ from prompt_toolkit.document import Document
19
20
  from prompt_toolkit.filters import (
20
21
  Always,
21
22
  Condition,
22
23
  Filter,
23
24
  FilterOrBool,
24
25
  has_focus,
26
+ is_true,
25
27
  to_filter,
26
28
  )
27
29
  from prompt_toolkit.formatted_text.base import to_formatted_text
@@ -39,13 +41,19 @@ from prompt_toolkit.layout.controls import (
39
41
  UIControl,
40
42
  )
41
43
  from prompt_toolkit.layout.dimension import Dimension
42
- from prompt_toolkit.layout.processors import AfterInput, ConditionalProcessor
44
+ from prompt_toolkit.layout.processors import (
45
+ AfterInput,
46
+ BeforeInput,
47
+ ConditionalProcessor,
48
+ PasswordProcessor,
49
+ Processor,
50
+ )
43
51
  from prompt_toolkit.layout.screen import WritePosition
44
52
  from prompt_toolkit.layout.utils import explode_text_fragments
53
+ from prompt_toolkit.lexers import DynamicLexer, Lexer
45
54
  from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
46
55
  from prompt_toolkit.utils import Event
47
56
  from prompt_toolkit.validation import Validator
48
- from prompt_toolkit.widgets.base import TextArea
49
57
 
50
58
  from euporie.core.app.current import get_app
51
59
  from euporie.core.border import InsetGrid
@@ -59,7 +67,8 @@ if TYPE_CHECKING:
59
67
  from collections.abc import Sequence
60
68
  from typing import Any, Callable
61
69
 
62
- from prompt_toolkit.buffer import Buffer, BufferAcceptHandler
70
+ from prompt_toolkit.auto_suggest import AutoSuggest
71
+ from prompt_toolkit.buffer import BufferAcceptHandler
63
72
  from prompt_toolkit.completion.base import Completer
64
73
  from prompt_toolkit.formatted_text.base import (
65
74
  AnyFormattedText,
@@ -498,6 +507,7 @@ class ExpandingBufferControl(BufferControl):
498
507
  focus_on_click: FilterOrBool = False,
499
508
  key_bindings: KeyBindingsBase | None = None,
500
509
  expand: FilterOrBool = True,
510
+ on_focus: Callable[[], None] | None = None,
501
511
  ) -> None:
502
512
  """Add an ``expand`` parameter to the buffer control."""
503
513
  super().__init__(
@@ -513,6 +523,7 @@ class ExpandingBufferControl(BufferControl):
513
523
  key_bindings=key_bindings,
514
524
  )
515
525
  self.expand = to_filter(expand)
526
+ self.on_focus = on_focus
516
527
 
517
528
  def preferred_width(self, max_available_width: int) -> int | None:
518
529
  """Enure text box expands to available width.
@@ -528,6 +539,16 @@ class ExpandingBufferControl(BufferControl):
528
539
  else:
529
540
  return None
530
541
 
542
+ def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
543
+ """Optionally call focus handler when focused."""
544
+ layout = get_app().layout
545
+ was_focused = layout.current_control == self
546
+ result = super().mouse_handler(mouse_event)
547
+ is_focused = layout.current_control == self
548
+ if not was_focused and is_focused and callable(self.on_focus):
549
+ self.on_focus()
550
+ return result
551
+
531
552
 
532
553
  class Text:
533
554
  """A text input widget."""
@@ -554,6 +575,9 @@ class Text:
554
575
  password: FilterOrBool = False,
555
576
  wrap_lines: FilterOrBool = False,
556
577
  prompt: AnyFormattedText | None = None,
578
+ on_focus: Callable[[], None] | None = None,
579
+ complete_while_typing: FilterOrBool = True,
580
+ auto_suggest: AutoSuggest | None = None,
557
581
  ) -> None:
558
582
  """Create a new text widget instance.
559
583
 
@@ -580,33 +604,44 @@ class Text:
580
604
  disabled: A filter which when evaluated to :py:const:`True` causes the
581
605
  widget to be disabled
582
606
  password: A filter to determine if the text input is a password field
583
- prompt: Text to display before the input
584
607
  wrap_lines: Whether to wrap lines wider than the text area
608
+ prompt: Text to display before the input
609
+ on_focus: Function to call when the buffer gains focus
610
+ complete_while_typing: Whether to show completions while typing
611
+ auto_suggest: Auto-suggestion behavior for the text input
585
612
  """
586
613
  self.style = style
587
614
  self.options = options or []
588
615
  self.disabled = to_filter(disabled)
589
-
590
616
  self.placeholder = placeholder
617
+ self.wrap_lines = wrap_lines
618
+ self.complete_while_typing = complete_while_typing
619
+ self.auto_suggest = auto_suggest
620
+ self.lexer = lexer
591
621
 
592
- self.text_area = TextArea(
593
- str(text),
622
+ self.buffer = Buffer(
623
+ document=Document(str(text), 0),
594
624
  multiline=multiline,
595
- height=Dimension(min=min_height, max=height, preferred=height),
596
- width=width,
597
- focusable=~self.disabled,
598
- focus_on_click=~self.disabled,
599
625
  read_only=self.disabled,
600
- style=f"{style} class:text,text-area",
601
- validator=Validator.from_callable(validation) if validation else None,
602
- accept_handler=accept_handler,
603
626
  completer=completer
604
627
  or ConditionalCompleter(
605
628
  WordCompleter(self.options),
606
629
  filter=Condition(lambda: bool(self.options)),
607
630
  ),
608
- lexer=lexer,
631
+ complete_while_typing=Condition(
632
+ lambda: is_true(self.complete_while_typing)
633
+ ),
634
+ validator=Validator.from_callable(validation) if validation else None,
635
+ validate_while_typing=validation is not None,
636
+ auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest),
637
+ accept_handler=accept_handler,
638
+ )
639
+
640
+ self.control = ExpandingBufferControl(
641
+ buffer=self.buffer,
642
+ lexer=DynamicLexer(lambda: self.lexer),
609
643
  input_processors=[
644
+ BeforeInput(prompt, style="class:text-area.prompt"),
610
645
  ConditionalProcessor(
611
646
  AfterInput(
612
647
  lambda: self.placeholder, style="class:text,placeholder"
@@ -615,58 +650,35 @@ class Text:
615
650
  lambda: self.placeholder is not None and self.buffer.text == ""
616
651
  ),
617
652
  ),
653
+ ConditionalProcessor(
654
+ processor=PasswordProcessor(), filter=to_filter(password)
655
+ ),
618
656
  *(input_processors or []),
619
657
  ],
620
- password=password,
621
- wrap_lines=wrap_lines,
622
- )
623
- self.buffer = self.text_area.buffer
624
-
625
- # Patch text area control's to expand to fill available width
626
- # Do this without monkey-pathing by sub-classing :class:`BufferControl` and
627
- # re-assigning the text-area's control
628
- self.text_area.control = ExpandingBufferControl(
629
- buffer=self.text_area.control.buffer,
630
- input_processors=self.text_area.control.input_processors,
631
- include_default_input_processors=(
632
- self.text_area.control.include_default_input_processors
633
- ),
634
- lexer=self.text_area.control.lexer,
635
- preview_search=self.text_area.control.preview_search,
636
- focusable=self.text_area.control.focusable,
637
- search_buffer_control=self.text_area.control._search_buffer_control,
638
- menu_position=self.text_area.control.menu_position,
639
- focus_on_click=self.text_area.control.focus_on_click,
640
- key_bindings=self.text_area.control.key_bindings,
658
+ focusable=~self.disabled,
659
+ focus_on_click=~self.disabled,
641
660
  expand=expand,
661
+ on_focus=on_focus,
642
662
  )
643
- window = self.text_area.window
644
- self.text_area.window = Window(
645
- height=window.height,
646
- width=window.width,
647
- dont_extend_height=window.dont_extend_height,
648
- dont_extend_width=window.dont_extend_width,
649
- content=self.text_area.control,
650
- style=window.style,
651
- wrap_lines=window.wrap_lines,
652
- left_margins=window.left_margins,
653
- right_margins=window.right_margins,
654
- get_line_prefix=window.get_line_prefix,
663
+
664
+ self.window = Window(
665
+ height=Dimension(min=min_height, max=height, preferred=height),
666
+ width=width,
667
+ content=self.control,
668
+ wrap_lines=Condition(lambda: is_true(self.wrap_lines)),
669
+ style=f"{style} class:text,text-area",
655
670
  )
656
- self.text_area.window.content = self.text_area.control
657
671
 
658
672
  if on_text_changed:
659
- self.text_area.buffer.on_text_changed += on_text_changed
673
+ self.buffer.on_text_changed += on_text_changed
660
674
  if validation:
661
- self.text_area.buffer.validate_while_typing = Always()
675
+ self.buffer.validate_while_typing = Always()
662
676
  self.container = Border(
663
677
  VSplit(
664
678
  [
665
- self.text_area,
679
+ self.window,
666
680
  ConditionalContainer(
667
- MarginContainer(
668
- ScrollbarMargin(), target=self.text_area.window
669
- ),
681
+ MarginContainer(ScrollbarMargin(), target=self.window),
670
682
  filter=to_filter(multiline),
671
683
  ),
672
684
  ]
@@ -678,7 +690,7 @@ class Text:
678
690
 
679
691
  def border_style(self) -> str:
680
692
  """Calculate the style to apply to the widget's border."""
681
- if self.text_area.buffer.validation_state == ValidationState.INVALID:
693
+ if self.buffer.validation_state == ValidationState.INVALID:
682
694
  return f"{self.style} class:text,border,invalid"
683
695
  else:
684
696
  return f"{self.style} class:text,border"
@@ -250,7 +250,10 @@ class KernelInput(TextArea):
250
250
  buffer=self.buffer,
251
251
  lexer=DynamicLexer(
252
252
  lambda: _get_lexer(
253
- app.config.syntax_highlighting, self.lexer, self.language
253
+ # Only lex buffers with text
254
+ self.buffer.text and app.config.syntax_highlighting,
255
+ self.lexer,
256
+ self.language,
254
257
  )
255
258
  ),
256
259
  input_processors=[
@@ -506,7 +509,7 @@ class StdInput:
506
509
  password=Condition(lambda: self.password),
507
510
  style="class:input",
508
511
  )
509
- self.window = text.text_area.window
512
+ self.window = text.window
510
513
  self.container = ConditionalContainer(
511
514
  LabelledWidget(
512
515
  body=text,
@@ -517,8 +520,8 @@ class StdInput:
517
520
 
518
521
  def accept(self, buffer: Buffer) -> bool:
519
522
  """Send the input to the kernel and hide the input box."""
520
- if self.kernel_tab.kernel.kc is not None:
521
- self.kernel_tab.kernel.kc.input(buffer.text)
523
+ if self.kernel_tab.kernel is not None:
524
+ self.kernel_tab.kernel.input(buffer.text)
522
525
  # Cleanup
523
526
  self.active = False
524
527