Skip to content

Commit a02abd0

Browse files
committed
Add syntax highlighting
1 parent 787733e commit a02abd0

File tree

2 files changed

+40
-14
lines changed

2 files changed

+40
-14
lines changed

examples/simple_form.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,20 @@ async def on_mount(self) -> None:
9898
placeholder="enter your age...",
9999
title="Age",
100100
)
101+
self.code = TextInput(
102+
name="code",
103+
placeholder="enter some python code...",
104+
title="Code",
105+
syntax="python",
106+
)
101107
self.output = Static(
102108
renderable=Panel(
103109
"", title="Report", border_style="blue", box=rich.box.SQUARE
104110
)
105111
)
106112

107113
await self.view.dock(self.output, edge="left", size=40)
108-
await self.view.dock(self.username, self.password, self.age, edge="top")
114+
await self.view.dock(self.username, self.password, self.age, self.code, edge="top")
109115

110116
async def action_next_tab_index(self) -> None:
111117
"""Changes the focus to the next form field"""
@@ -125,6 +131,7 @@ async def action_submit(self) -> None:
125131
username: {self.username.value}
126132
password: {"".join("•" for _ in self.password.value)}
127133
age: {self.age.value}
134+
code: {self.code.value}
128135
"""
129136
await self.output.update(
130137
Panel(formatted, title="Report", border_style="blue", box=rich.box.SQUARE)

src/textual_inputs/text_input.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,21 @@
1515

1616
from textual_inputs.events import InputOnChange, InputOnFocus
1717

18+
from rich.console import Console
19+
from rich.syntax import Syntax
20+
1821
if TYPE_CHECKING:
1922
from rich.console import RenderableType
2023

24+
CONSOLE = Console()
25+
26+
27+
def get_syntax(code, syntax):
28+
syntax = Syntax(code, syntax)
29+
with CONSOLE.capture() as capture:
30+
CONSOLE.print(syntax)
31+
return capture.get()
32+
2133

2234
class TextInput(Widget):
2335
"""
@@ -39,6 +51,7 @@ class TextInput(Widget):
3951
placeholder (str): The placeholder message.
4052
title (str): The displayed title of the widget.
4153
has_password (bool): True if the text field masks the input.
54+
syntax (Optional[str]): the name of the language for syntax highlighting.
4255
has_focus (bool): True if the widget is focused.
4356
cursor (Tuple[str, Style]): The character used for the cursor
4457
and a rich Style object defining its appearance.
@@ -81,13 +94,15 @@ def __init__(
8194
placeholder: str = "",
8295
title: str = "",
8396
password: bool = False,
97+
syntax: Optional[str] = None,
8498
**kwargs: Any,
8599
) -> None:
86100
super().__init__(name, **kwargs)
87101
self.value = value
88102
self.placeholder = placeholder
89103
self.title = title
90104
self.has_password = password
105+
self.syntax = syntax
91106
self._cursor_position = len(self.value)
92107

93108
def __rich_repr__(self):
@@ -142,13 +157,15 @@ def render(self) -> RenderableType:
142157
box=rich.box.DOUBLE if self.has_focus else rich.box.SQUARE,
143158
)
144159

145-
def _conceal_or_reveal(self, segment: str) -> str:
160+
def _conceal_or_reveal(self, segment: str) -> Union[str, Text]:
146161
"""
147-
Produce the segment either concealed like a password or as it
148-
was passed.
162+
Produce the segment concealed like a password, as it was passed,
163+
or syntax highlighted.
149164
"""
150165
if self.has_password:
151-
return "".join("•" for _ in segment)
166+
return "•" * len(segment)
167+
if self.syntax:
168+
return Text.from_ansi(get_syntax(segment, self.syntax))
152169
return segment
153170

154171
def _render_text_with_cursor(self) -> List[Union[str, Tuple[str, Style]]]:
@@ -157,16 +174,18 @@ def _render_text_with_cursor(self) -> List[Union[str, Tuple[str, Style]]]:
157174
"""
158175
if len(self.value) == 0:
159176
segments = [self.cursor]
160-
elif self._cursor_position == 0:
161-
segments = [self.cursor, self._conceal_or_reveal(self.value)]
162-
elif self._cursor_position == len(self.value):
163-
segments = [self._conceal_or_reveal(self.value), self.cursor]
164177
else:
165-
segments = [
166-
self._conceal_or_reveal(self.value[: self._cursor_position]),
167-
self.cursor,
168-
self._conceal_or_reveal(self.value[self._cursor_position :]),
169-
]
178+
text = self._conceal_or_reveal(self.value)
179+
if self._cursor_position == 0:
180+
segments = [self.cursor, text]
181+
elif self._cursor_position == len(self.value):
182+
segments = [text, self.cursor]
183+
else:
184+
segments = [
185+
text[: self._cursor_position],
186+
self.cursor,
187+
text[self._cursor_position :],
188+
]
170189

171190
return segments
172191

0 commit comments

Comments
 (0)