Skip to content

Commit

Permalink
copy to clipboard
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Jan 3, 2025
1 parent 273cc04 commit 05a7128
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/textual/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ def __init__(
self._no_wrap = no_wrap
self._ellipsis = ellipsis

def __str__(self) -> str:
return self._text

@classmethod
def from_rich_text(
cls,
Expand Down
25 changes: 25 additions & 0 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ class Screen(Generic[ScreenResultType], Widget):
BINDINGS = [
Binding("tab", "app.focus_next", "Focus Next", show=False),
Binding("shift+tab", "app.focus_previous", "Focus Previous", show=False),
Binding("ctrl+c", "screen.copy_text", "Copy selected text", show=False),
]

def __init__(
Expand Down Expand Up @@ -833,6 +834,30 @@ def minimize(self) -> None:
self.scroll_to_widget, self.focused, animate=False, center=True
)

def get_selected_text(self) -> str | None:
"""Get text under selection.
Returns:
Selected text, or `None` if no text was selected.
"""
if not self.selections:
return None

widget_text: list[str] = []
for widget, selection in self.selections.items():
selected_text_in_widget = widget.get_selection(selection)
if selected_text_in_widget is not None:
widget_text.append(selected_text_in_widget)

selected_text = "\n".join(widget_text)
return selected_text

def action_copy_text(self) -> None:
"""Copy selected text to clipboard."""
selection = self.get_selected_text()
if selection is not None:
self.app.copy_to_clipboard(selection)

def action_maximize(self) -> None:
"""Action to maximize the currently focused widget."""
if self.focused is not None:
Expand Down
32 changes: 32 additions & 0 deletions src/textual/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,38 @@ def from_offsets(cls, offset1: Offset, offset2: Offset) -> Selection:
offsets = sorted([offset1, offset2], key=(lambda offset: (offset.y, offset.x)))
return cls(*offsets)

def extract(self, text: str) -> str:
"""Extract selection from text.
Args:
text: Raw text pulled from widget.
Returns:
Extracted text.
"""
lines = text.splitlines()
if self.start is None:
start_line = 0
start_offset = 0
else:
start_line, start_offset = self.start.transpose

if self.end is None:
end_line = len(lines) - 1
end_offset = len(lines[end_line])
else:
end_line, end_offset = self.end.transpose

if start_line == end_line:
return lines[start_line][start_offset:end_offset]

selection: list[str] = []
first_line, *mid_lines, last_line = lines[start_line:end_line]
selection.append(first_line[start_offset:])
selection.extend(mid_lines)
selection.append(last_line[: end_offset + 1])
return "\n".join(selection)

def get_span(self, y: int) -> tuple[int, int] | None:
"""Get the selected span in a given line.
Expand Down
17 changes: 17 additions & 0 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
from textual.box_model import BoxModel
from textual.cache import FIFOCache
from textual.color import Color
from textual.content import Content
from textual.css.match import match
from textual.css.parse import parse_selectors
from textual.css.query import NoMatches, WrongType
Expand Down Expand Up @@ -3810,6 +3811,22 @@ def visual_style(self) -> VisualStyle:
strike=style.strike,
)

def get_selection(self, selection: Selection) -> str | None:
"""Get the text under the selection.
Args:
selection: Selection information.
Returns:
Extracted text, or `None` if no text could be extracted.
"""
visual = self._render()
if isinstance(visual, (Text, Content)):
text = str(visual)
else:
return None
return selection.extract(text)

def _render_content(self) -> None:
"""Render all lines."""
width, height = self.size
Expand Down

0 comments on commit 05a7128

Please sign in to comment.