Skip to content

Commit f481ed5

Browse files
committed
log selection
1 parent f2b4764 commit f481ed5

File tree

3 files changed

+61
-7
lines changed

3 files changed

+61
-7
lines changed

src/textual/screen.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,7 @@ def action_copy_text(self) -> None:
870870
selection = self.get_selected_text()
871871
if selection is not None:
872872
self.app.copy_to_clipboard(selection)
873+
self.notify(selection)
873874

874875
def action_maximize(self) -> None:
875876
"""Action to maximize the currently focused widget."""

src/textual/strip.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,3 +672,24 @@ def text_align(self, width: int, align: AlignHorizontal) -> Strip:
672672
line_pad(self._segments, width - self.cell_length, 0, Style.null()),
673673
width,
674674
)
675+
676+
def apply_offsets(self, x: int, y: int) -> Strip:
677+
"""Apply offsets used in text selection.
678+
679+
Args:
680+
x: Offset on X axis (column).
681+
y: Offset on Y axis (row).
682+
683+
Returns:
684+
New strip.
685+
"""
686+
segments = self._segments
687+
strip_segments: list[Segment] = []
688+
for segment in segments:
689+
text, style, _ = segment
690+
offset_style = Style.from_meta({"offset": (x, y)})
691+
strip_segments.append(
692+
Segment(text, style + offset_style if style else offset_style)
693+
)
694+
x += len(segment.text)
695+
return Strip(strip_segments, self._cell_length)

src/textual/widgets/_log.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from rich.cells import cell_len
77
from rich.highlighter import Highlighter, ReprHighlighter
8-
from rich.segment import Segment
98
from rich.style import Style
109
from rich.text import Text
1110

@@ -15,6 +14,7 @@
1514
from textual.geometry import Size
1615
from textual.reactive import var
1716
from textual.scroll_view import ScrollView
17+
from textual.selection import Selection
1818
from textual.strip import Strip
1919

2020
if TYPE_CHECKING:
@@ -26,6 +26,7 @@
2626
class Log(ScrollView, can_focus=True):
2727
"""A widget to log text."""
2828

29+
ALLOW_SELECT = True
2930
DEFAULT_CSS = """
3031
Log {
3132
background: $surface;
@@ -75,6 +76,11 @@ def __init__(
7576
self._render_line_cache: LRUCache[int, Strip] = LRUCache(1024)
7677
self.highlighter: Highlighter = ReprHighlighter()
7778
"""The Rich Highlighter object to use, if `highlight=True`"""
79+
self._clear_y = 0
80+
81+
@property
82+
def allow_select(self) -> bool:
83+
return True
7884

7985
@property
8086
def lines(self) -> Sequence[str]:
@@ -251,8 +257,21 @@ def clear(self) -> Self:
251257
self._render_line_cache.clear()
252258
self._updates += 1
253259
self.virtual_size = Size(0, 0)
260+
self._clear_y = 0
254261
return self
255262

263+
def get_selection(self, selection: Selection) -> tuple[str, str] | None:
264+
"""Get the text under the selection.
265+
266+
Args:
267+
selection: Selection information.
268+
269+
Returns:
270+
Tuple of extracted text and ending (typically "\n" or " "), or `None` if no text could be extracted.
271+
"""
272+
text = "\n".join(self._lines)
273+
return selection.extract(text), "\n"
274+
256275
def render_line(self, y: int) -> Strip:
257276
"""Render a line of content.
258277
@@ -284,6 +303,7 @@ def _render_line(self, y: int, scroll_x: int, width: int) -> Strip:
284303
line = self._render_line_strip(y, rich_style)
285304
assert line._cell_length is not None
286305
line = line.crop_extend(scroll_x, scroll_x + width, rich_style)
306+
line = line.apply_offsets(scroll_x, y)
287307
return line
288308

289309
def _render_line_strip(self, y: int, rich_style: Style) -> Strip:
@@ -296,18 +316,30 @@ def _render_line_strip(self, y: int, rich_style: Style) -> Strip:
296316
Returns:
297317
An uncropped Strip.
298318
"""
299-
if y in self._render_line_cache:
319+
selection = self.selection
320+
if y in self._render_line_cache and self.selection is None:
300321
return self._render_line_cache[y]
301322

302323
_line = self._process_line(self._lines[y])
303324

325+
line_text = Text(_line, style=rich_style, no_wrap=True)
304326
if self.highlight:
305-
line_text = self.highlighter(Text(_line, style=rich_style, no_wrap=True))
306-
line = Strip(line_text.render(self.app.console), cell_len(_line))
307-
else:
308-
line = Strip([Segment(_line, rich_style)], cell_len(_line))
327+
line_text = self.highlighter(line_text)
328+
if selection is not None:
329+
if (select_span := selection.get_span(y - self._clear_y)) is not None:
330+
start, end = select_span
331+
if end == -1:
332+
end = len(line_text)
333+
334+
selection_style = self.screen.get_component_rich_style(
335+
"screen--selection"
336+
)
337+
line_text.stylize(selection_style, start, end)
338+
339+
line = Strip(line_text.render(self.app.console), cell_len(_line))
309340

310-
self._render_line_cache[y] = line
341+
if selection is not None:
342+
self._render_line_cache[y] = line
311343
return line
312344

313345
def refresh_lines(self, y_start: int, line_count: int = 1) -> None:

0 commit comments

Comments
 (0)