mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Iterator, Sequence, overload
|
from typing import TYPE_CHECKING, Any, Iterator, Sequence, overload
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ class DuplicateIds(Exception):
|
|||||||
|
|
||||||
|
|
||||||
@rich.repr.auto(angular=True)
|
@rich.repr.auto(angular=True)
|
||||||
class NodeList(Sequence):
|
class NodeList(Sequence["Widget"]):
|
||||||
"""
|
"""
|
||||||
A container for widgets that forms one level of hierarchy.
|
A container for widgets that forms one level of hierarchy.
|
||||||
|
|
||||||
@@ -46,10 +47,10 @@ class NodeList(Sequence):
|
|||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return len(self._nodes)
|
return len(self._nodes)
|
||||||
|
|
||||||
def __contains__(self, widget: Widget) -> bool:
|
def __contains__(self, widget: object) -> bool:
|
||||||
return widget in self._nodes
|
return widget in self._nodes
|
||||||
|
|
||||||
def index(self, widget: Widget) -> int:
|
def index(self, widget: Any, start: int = 0, stop: int = sys.maxsize) -> int:
|
||||||
"""Return the index of the given widget.
|
"""Return the index of the given widget.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -61,7 +62,7 @@ class NodeList(Sequence):
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If the widget is not in the node list.
|
ValueError: If the widget is not in the node list.
|
||||||
"""
|
"""
|
||||||
return self._nodes.index(widget)
|
return self._nodes.index(widget, start, stop)
|
||||||
|
|
||||||
def _get_by_id(self, widget_id: str) -> Widget | None:
|
def _get_by_id(self, widget_id: str) -> Widget | None:
|
||||||
"""Get the widget for the given widget_id, or None if there's no matches in this list"""
|
"""Get the widget for the given widget_id, or None if there's no matches in this list"""
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ from rich.console import ConsoleOptions, Console, RenderResult
|
|||||||
from rich.traceback import Traceback
|
from rich.traceback import Traceback
|
||||||
|
|
||||||
from ._help_renderables import HelpText
|
from ._help_renderables import HelpText
|
||||||
from .tokenize import Token
|
from .tokenizer import Token, TokenError
|
||||||
from .tokenizer import TokenError
|
|
||||||
|
|
||||||
|
|
||||||
class DeclarationError(Exception):
|
class DeclarationError(Exception):
|
||||||
@@ -32,7 +31,7 @@ class StyleValueError(ValueError):
|
|||||||
error is raised.
|
error is raised.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, help_text: HelpText | None = None):
|
def __init__(self, *args: object, help_text: HelpText | None = None):
|
||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
self.help_text = help_text
|
self.help_text = help_text
|
||||||
|
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ class Tokenizer:
|
|||||||
while True:
|
while True:
|
||||||
if line_no >= len(self.lines):
|
if line_no >= len(self.lines):
|
||||||
raise EOFError(
|
raise EOFError(
|
||||||
self.path, self.code, line_no, col_no, "Unexpected end of file"
|
self.path, self.code, (line_no, col_no), "Unexpected end of file"
|
||||||
)
|
)
|
||||||
line = self.lines[line_no]
|
line = self.lines[line_no]
|
||||||
match = expect.search(line, col_no)
|
match = expect.search(line, col_no)
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ async def _on_startup(app: Application) -> None:
|
|||||||
def _run_devtools(verbose: bool, exclude: list[str] | None = None) -> None:
|
def _run_devtools(verbose: bool, exclude: list[str] | None = None) -> None:
|
||||||
app = _make_devtools_aiohttp_app(verbose=verbose, exclude=exclude)
|
app = _make_devtools_aiohttp_app(verbose=verbose, exclude=exclude)
|
||||||
|
|
||||||
def noop_print(_: str):
|
def noop_print(_: str) -> None:
|
||||||
return None
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
run_app(
|
run_app(
|
||||||
@@ -77,7 +77,3 @@ def _make_devtools_aiohttp_app(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
_run_devtools()
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class Offset(NamedTuple):
|
|||||||
"""
|
"""
|
||||||
x1, y1 = self
|
x1, y1 = self
|
||||||
x2, y2 = other
|
x2, y2 = other
|
||||||
distance = ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) ** 0.5
|
distance: float = ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) ** 0.5
|
||||||
return distance
|
return distance
|
||||||
|
|
||||||
|
|
||||||
@@ -217,6 +217,8 @@ class Size(NamedTuple):
|
|||||||
|
|
||||||
def __contains__(self, other: Any) -> bool:
|
def __contains__(self, other: Any) -> bool:
|
||||||
try:
|
try:
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
x, y = other
|
x, y = other
|
||||||
except Exception:
|
except Exception:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
|
|||||||
@@ -3,24 +3,23 @@ from __future__ import annotations
|
|||||||
import rich.repr
|
import rich.repr
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import TYPE_CHECKING
|
from typing import Generic
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
from .app import App, ReturnType
|
||||||
from .app import App
|
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto(angular=True)
|
@rich.repr.auto(angular=True)
|
||||||
class Pilot:
|
class Pilot(Generic[ReturnType]):
|
||||||
"""Pilot object to drive an app."""
|
"""Pilot object to drive an app."""
|
||||||
|
|
||||||
def __init__(self, app: App) -> None:
|
def __init__(self, app: App[ReturnType]) -> None:
|
||||||
self._app = app
|
self._app = app
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield "app", self._app
|
yield "app", self._app
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def app(self) -> App:
|
def app(self) -> App[ReturnType]:
|
||||||
"""App: A reference to the application."""
|
"""App: A reference to the application."""
|
||||||
return self._app
|
return self._app
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ class Pilot:
|
|||||||
"""Wait for any animation to complete."""
|
"""Wait for any animation to complete."""
|
||||||
await self._app.animator.wait_for_idle()
|
await self._app.animator.wait_for_idle()
|
||||||
|
|
||||||
async def exit(self, result: object) -> None:
|
async def exit(self, result: ReturnType) -> None:
|
||||||
"""Exit the app with the given result.
|
"""Exit the app with the given result.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import statistics
|
import statistics
|
||||||
from typing import Sequence, Iterable, Callable, TypeVar
|
from typing import Generic, Sequence, Iterable, Callable, TypeVar
|
||||||
|
|
||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
from rich.console import ConsoleOptions, Console, RenderResult
|
from rich.console import ConsoleOptions, Console, RenderResult
|
||||||
@@ -12,8 +12,10 @@ from textual.renderables._blend_colors import blend_colors
|
|||||||
|
|
||||||
T = TypeVar("T", int, float)
|
T = TypeVar("T", int, float)
|
||||||
|
|
||||||
|
SummaryFunction = Callable[[Sequence[T]], float]
|
||||||
|
|
||||||
class Sparkline:
|
|
||||||
|
class Sparkline(Generic[T]):
|
||||||
"""A sparkline representing a series of data.
|
"""A sparkline representing a series of data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -33,16 +35,16 @@ class Sparkline:
|
|||||||
width: int | None,
|
width: int | None,
|
||||||
min_color: Color = Color.from_rgb(0, 255, 0),
|
min_color: Color = Color.from_rgb(0, 255, 0),
|
||||||
max_color: Color = Color.from_rgb(255, 0, 0),
|
max_color: Color = Color.from_rgb(255, 0, 0),
|
||||||
summary_function: Callable[[list[T]], float] = max,
|
summary_function: SummaryFunction[T] = max,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.data = data
|
self.data: Sequence[T] = data
|
||||||
self.width = width
|
self.width = width
|
||||||
self.min_color = Style.from_color(min_color)
|
self.min_color = Style.from_color(min_color)
|
||||||
self.max_color = Style.from_color(max_color)
|
self.max_color = Style.from_color(max_color)
|
||||||
self.summary_function = summary_function
|
self.summary_function: SummaryFunction[T] = summary_function
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _buckets(cls, data: Sequence[T], num_buckets: int) -> Iterable[list[T]]:
|
def _buckets(cls, data: Sequence[T], num_buckets: int) -> Iterable[Sequence[T]]:
|
||||||
"""Partition ``data`` into ``num_buckets`` buckets. For example, the data
|
"""Partition ``data`` into ``num_buckets`` buckets. For example, the data
|
||||||
[1, 2, 3, 4] partitioned into 2 buckets is [[1, 2], [3, 4]].
|
[1, 2, 3, 4] partitioned into 2 buckets is [[1, 2], [3, 4]].
|
||||||
|
|
||||||
@@ -73,13 +75,15 @@ class Sparkline:
|
|||||||
minimum, maximum = min(self.data), max(self.data)
|
minimum, maximum = min(self.data), max(self.data)
|
||||||
extent = maximum - minimum or 1
|
extent = maximum - minimum or 1
|
||||||
|
|
||||||
buckets = list(self._buckets(self.data, num_buckets=self.width))
|
buckets = tuple(self._buckets(self.data, num_buckets=width))
|
||||||
|
|
||||||
bucket_index = 0
|
bucket_index = 0.0
|
||||||
bars_rendered = 0
|
bars_rendered = 0
|
||||||
step = len(buckets) / width
|
step = len(buckets) / width
|
||||||
summary_function = self.summary_function
|
summary_function = self.summary_function
|
||||||
min_color, max_color = self.min_color.color, self.max_color.color
|
min_color, max_color = self.min_color.color, self.max_color.color
|
||||||
|
assert min_color is not None
|
||||||
|
assert max_color is not None
|
||||||
while bars_rendered < width:
|
while bars_rendered < width:
|
||||||
partition = buckets[int(bucket_index)]
|
partition = buckets[int(bucket_index)]
|
||||||
partition_summary = summary_function(partition)
|
partition_summary = summary_function(partition)
|
||||||
@@ -94,10 +98,16 @@ class Sparkline:
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
def last(l):
|
def last(l: Sequence[T]) -> T:
|
||||||
return l[-1]
|
return l[-1]
|
||||||
|
|
||||||
funcs = min, max, last, statistics.median, statistics.mean
|
funcs: Sequence[SummaryFunction[int]] = (
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
last,
|
||||||
|
statistics.median,
|
||||||
|
statistics.mean,
|
||||||
|
)
|
||||||
nums = [10, 2, 30, 60, 45, 20, 7, 8, 9, 10, 50, 13, 10, 6, 5, 4, 3, 7, 20]
|
nums = [10, 2, 30, 60, 45, 20, 7, 8, 9, 10, 50, 13, 10, 6, 5, 4, 3, 7, 20]
|
||||||
console.print(f"data = {nums}\n")
|
console.print(f"data = {nums}\n")
|
||||||
for f in funcs:
|
for f in funcs:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import functools
|
import functools
|
||||||
from typing import Iterable
|
from typing import Iterable, Tuple, cast
|
||||||
|
|
||||||
from rich.cells import cell_len
|
from rich.cells import cell_len
|
||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
@@ -62,12 +62,17 @@ class TextOpacity:
|
|||||||
_Segment = Segment
|
_Segment = Segment
|
||||||
_from_color = Style.from_color
|
_from_color = Style.from_color
|
||||||
if opacity == 0:
|
if opacity == 0:
|
||||||
for text, style, control in segments:
|
for text, style, control in cast(
|
||||||
|
# use Tuple rather than tuple so Python 3.7 doesn't complain
|
||||||
|
Iterable[Tuple[str, Style, object]],
|
||||||
|
segments,
|
||||||
|
):
|
||||||
invisible_style = _from_color(bgcolor=style.bgcolor)
|
invisible_style = _from_color(bgcolor=style.bgcolor)
|
||||||
yield _Segment(cell_len(text) * " ", invisible_style)
|
yield _Segment(cell_len(text) * " ", invisible_style)
|
||||||
else:
|
else:
|
||||||
for segment in segments:
|
for segment in segments:
|
||||||
text, style, control = segment
|
# use Tuple rather than tuple so Python 3.7 doesn't complain
|
||||||
|
text, style, control = cast(Tuple[str, Style, object], segment)
|
||||||
if not style:
|
if not style:
|
||||||
yield segment
|
yield segment
|
||||||
continue
|
continue
|
||||||
@@ -85,40 +90,3 @@ class TextOpacity:
|
|||||||
) -> RenderResult:
|
) -> RenderResult:
|
||||||
segments = console.render(self.renderable, options)
|
segments = console.render(self.renderable, options)
|
||||||
return self.process_segments(segments, self.opacity)
|
return self.process_segments(segments, self.opacity)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
from rich.live import Live
|
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.text import Text
|
|
||||||
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
console = Console()
|
|
||||||
|
|
||||||
panel = Panel.fit(
|
|
||||||
Text("Steak: £30", style="#fcffde on #03761e"),
|
|
||||||
title="Menu",
|
|
||||||
style="#ffffff on #000000",
|
|
||||||
)
|
|
||||||
console.print(panel)
|
|
||||||
|
|
||||||
opacity_panel = TextOpacity(panel, opacity=0.5)
|
|
||||||
console.print(opacity_panel)
|
|
||||||
|
|
||||||
def frange(start, end, step):
|
|
||||||
current = start
|
|
||||||
while current < end:
|
|
||||||
yield current
|
|
||||||
current += step
|
|
||||||
|
|
||||||
while current >= 0:
|
|
||||||
yield current
|
|
||||||
current -= step
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
|
|
||||||
with Live(opacity_panel, refresh_per_second=60) as live:
|
|
||||||
for value in itertools.cycle(frange(0, 1, 0.05)):
|
|
||||||
opacity_panel.value = value
|
|
||||||
sleep(0.05)
|
|
||||||
|
|||||||
@@ -109,8 +109,8 @@ class Strip:
|
|||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return len(self._segments)
|
return len(self._segments)
|
||||||
|
|
||||||
def __eq__(self, strip: Strip) -> bool:
|
def __eq__(self, strip: object) -> bool:
|
||||||
return (
|
return isinstance(strip, Strip) and (
|
||||||
self._segments == strip._segments and self.cell_length == strip.cell_length
|
self._segments == strip._segments and self.cell_length == strip.cell_length
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ TOGGLE_STYLE = Style.from_meta({"toggle": True})
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _TreeLine:
|
class _TreeLine(Generic[TreeDataType]):
|
||||||
path: list[TreeNode]
|
path: list[TreeNode[TreeDataType]]
|
||||||
last: bool
|
last: bool
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def node(self) -> TreeNode:
|
def node(self) -> TreeNode[TreeDataType]:
|
||||||
"""TreeNode: The node associated with this line."""
|
"""TreeNode: The node associated with this line."""
|
||||||
return self.path[-1]
|
return self.path[-1]
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ class TreeNode(Generic[TreeDataType]):
|
|||||||
self._label = tree.process_label(label)
|
self._label = tree.process_label(label)
|
||||||
self.data = data
|
self.data = data
|
||||||
self._expanded = expanded
|
self._expanded = expanded
|
||||||
self._children: list[TreeNode] = []
|
self._children: list[TreeNode[TreeDataType]] = []
|
||||||
|
|
||||||
self._hover_ = False
|
self._hover_ = False
|
||||||
self._selected_ = False
|
self._selected_ = False
|
||||||
@@ -475,11 +475,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
self._updates += 1
|
self._updates += 1
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def select_node(self, node: TreeNode | None) -> None:
|
def select_node(self, node: TreeNode[TreeDataType] | None) -> None:
|
||||||
"""Move the cursor to the given node, or reset cursor.
|
"""Move the cursor to the given node, or reset cursor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
node (TreeNode | None): A tree node, or None to reset cursor.
|
node (TreeNode[TreeDataType] | None): A tree node, or None to reset cursor.
|
||||||
"""
|
"""
|
||||||
self.cursor_line = -1 if node is None else node._line
|
self.cursor_line = -1 if node is None else node._line
|
||||||
|
|
||||||
@@ -579,11 +579,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
"""
|
"""
|
||||||
self.scroll_to_region(Region(0, line, self.size.width, 1))
|
self.scroll_to_region(Region(0, line, self.size.width, 1))
|
||||||
|
|
||||||
def scroll_to_node(self, node: TreeNode) -> None:
|
def scroll_to_node(self, node: TreeNode[TreeDataType]) -> None:
|
||||||
"""Scroll to the given node.
|
"""Scroll to the given node.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
node (TreeNode): Node to scroll in to view.
|
node (TreeNode[TreeDataType]): Node to scroll in to view.
|
||||||
"""
|
"""
|
||||||
line = node._line
|
line = node._line
|
||||||
if line != -1:
|
if line != -1:
|
||||||
@@ -637,7 +637,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
|
|
||||||
root = self.root
|
root = self.root
|
||||||
|
|
||||||
def add_node(path: list[TreeNode], node: TreeNode, last: bool) -> None:
|
def add_node(
|
||||||
|
path: list[TreeNode[TreeDataType]], node: TreeNode[TreeDataType], last: bool
|
||||||
|
) -> None:
|
||||||
child_path = [*path, node]
|
child_path = [*path, node]
|
||||||
node._line = len(lines)
|
node._line = len(lines)
|
||||||
add_line(TreeLine(child_path, last))
|
add_line(TreeLine(child_path, last))
|
||||||
|
|||||||
Reference in New Issue
Block a user