Skip to content

Commit fb31b5f

Browse files
authored
Merge pull request #14 from davidbrochart/syntax
- add syntax highlighting
2 parents 48b20f1 + d97dbf5 commit fb31b5f

File tree

2 files changed

+47
-22
lines changed

2 files changed

+47
-22
lines changed

examples/simple_form.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class SimpleForm(App):
7575

7676
def __init__(self, **kwargs) -> None:
7777
super().__init__(**kwargs)
78-
self.tab_index = ["username", "password", "age"]
78+
self.tab_index = ["username", "password", "age", "code"]
7979

8080
async def on_load(self) -> None:
8181
await self.bind("q", "quit", "Quit")
@@ -109,14 +109,22 @@ async def on_mount(self) -> None:
109109
title="Age",
110110
)
111111
self.age.on_change_handler_name = "handle_age_on_change"
112+
113+
self.code = TextInput(
114+
name="code",
115+
placeholder="enter some python code...",
116+
title="Code",
117+
syntax="python",
118+
)
119+
self.code.on_change_handler_name = "handle_code_on_change"
112120

113121
self.output = Static(
114122
renderable=Panel(
115123
"", title="Report", border_style="blue", box=rich.box.SQUARE
116124
)
117125
)
118126
await self.view.dock(self.output, edge="left", size=40)
119-
await self.view.dock(self.username, self.password, self.age, edge="top")
127+
await self.view.dock(self.username, self.password, self.age, self.code, edge="top")
120128

121129
async def action_next_tab_index(self) -> None:
122130
"""Changes the focus to the next form field"""
@@ -136,6 +144,7 @@ async def action_submit(self) -> None:
136144
username: {self.username.value}
137145
password: {"".join("•" for _ in self.password.value)}
138146
age: {self.age.value}
147+
code: {self.code.value}
139148
"""
140149
await self.output.update(
141150
Panel(formatted, title="Report", border_style="blue", box=rich.box.SQUARE)

src/textual_inputs/text_input.py

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,27 @@
1515

1616
from textual_inputs.events import InputOnChange, InputOnFocus, make_message_class
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 conceal_text(segment: str) -> str:
28+
"""Produce the segment concealed like a password."""
29+
return "•" * len(segment)
30+
31+
32+
def syntax_highlight_text(code: str, syntax: str) -> Text:
33+
"""Produces highlighted text based on the syntax."""
34+
syntax_obj = Syntax(code, syntax)
35+
with CONSOLE.capture() as capture:
36+
CONSOLE.print(syntax_obj)
37+
return Text.from_ansi(capture.get())
38+
2139

2240
class TextInput(Widget):
2341
"""
@@ -33,12 +51,14 @@ class TextInput(Widget):
3351
of the widget's border.
3452
password (bool, optional): Defaults to False. Hides the text
3553
input, replacing it with bullets.
54+
syntax (Optional[str]): the name of the language for syntax highlighting.
3655
3756
Attributes:
3857
value (str): the value of the text field
3958
placeholder (str): The placeholder message.
4059
title (str): The displayed title of the widget.
4160
has_password (bool): True if the text field masks the input.
61+
syntax (Optional[str]): the name of the language for syntax highlighting.
4262
has_focus (bool): True if the widget is focused.
4363
cursor (Tuple[str, Style]): The character used for the cursor
4464
and a rich Style object defining its appearance.
@@ -87,12 +107,15 @@ def __init__(
87107
placeholder: str = "",
88108
title: str = "",
89109
password: bool = False,
110+
syntax: Optional[str] = None,
111+
**kwargs: Any,
90112
) -> None:
91113
super().__init__(name)
92114
self.value = value
93115
self.placeholder = placeholder
94116
self.title = title
95117
self.has_password = password
118+
self.syntax = syntax
96119
self._on_change_message_class = InputOnChange
97120
self._on_focus_message_class = InputOnFocus
98121
self._cursor_position = len(self.value)
@@ -153,7 +176,7 @@ def render(self) -> RenderableType:
153176
else:
154177
segments = [self.placeholder]
155178
else:
156-
segments = [self._conceal_or_reveal(self.value)]
179+
segments = [self._modify_text(self.value)]
157180

158181
text = Text.assemble(*segments)
159182

@@ -177,33 +200,26 @@ def render(self) -> RenderableType:
177200
box=rich.box.DOUBLE if self.has_focus else rich.box.SQUARE,
178201
)
179202

180-
def _conceal_or_reveal(self, segment: str) -> str:
203+
def _modify_text(self, segment: str) -> Union[str, Text]:
181204
"""
182-
Produce the segment either concealed like a password or as it
183-
was passed.
205+
Produces the text with modifications, such as password concealing.
184206
"""
185207
if self.has_password:
186-
return "".join("•" for _ in segment)
208+
return conceal_text(segment)
209+
if self.syntax:
210+
return syntax_highlight_text(segment, self.syntax)
187211
return segment
188212

189-
def _render_text_with_cursor(self) -> List[Union[str, Tuple[str, Style]]]:
213+
def _render_text_with_cursor(self) -> List[Union[str, Text, Tuple[str, Style]]]:
190214
"""
191215
Produces the renderable Text object combining value and cursor
192216
"""
193-
if len(self.value) == 0:
194-
segments = [self.cursor]
195-
elif self._cursor_position == 0:
196-
segments = [self.cursor, self._conceal_or_reveal(self.value)]
197-
elif self._cursor_position == len(self.value):
198-
segments = [self._conceal_or_reveal(self.value), self.cursor]
199-
else:
200-
segments = [
201-
self._conceal_or_reveal(self.value[: self._cursor_position]),
202-
self.cursor,
203-
self._conceal_or_reveal(self.value[self._cursor_position :]),
204-
]
205-
206-
return segments
217+
text = self._modify_text(self.value)
218+
return [
219+
text[: self._cursor_position],
220+
self.cursor,
221+
text[self._cursor_position :],
222+
]
207223

208224
async def on_focus(self, event: events.Focus) -> None:
209225
self._has_focus = True

0 commit comments

Comments
 (0)