mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
code browser and fixes
This commit is contained in:
26
examples/code_browser.css
Normal file
26
examples/code_browser.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#tree-view {
|
||||||
|
display: none;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeBrowser.-show-tree #tree-view {
|
||||||
|
display: block;
|
||||||
|
dock: left;
|
||||||
|
height: 100%;
|
||||||
|
width: auto;
|
||||||
|
background: $surface;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeBrowser{
|
||||||
|
background: $surface-darken-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectoryTree {
|
||||||
|
padding-right: 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#code {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
49
examples/code_browser.py
Normal file
49
examples/code_browser.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from rich.syntax import Syntax
|
||||||
|
from rich.traceback import Traceback
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.layout import Container, Vertical
|
||||||
|
from textual.reactive import Reactive
|
||||||
|
from textual.widgets import DirectoryTree, Footer, Header, Static
|
||||||
|
|
||||||
|
|
||||||
|
class CodeBrowser(App):
|
||||||
|
|
||||||
|
show_tree = Reactive.init(True)
|
||||||
|
|
||||||
|
def watch_show_tree(self, show_tree: bool) -> None:
|
||||||
|
self.set_class(show_tree, "-show-tree")
|
||||||
|
|
||||||
|
def on_load(self) -> None:
|
||||||
|
self.bind("t", "toggle_tree", description="Toggle Tree")
|
||||||
|
self.bind("q", "quit", description="Quit")
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
path = "./" if len(sys.argv) < 2 else sys.argv[1]
|
||||||
|
yield Header()
|
||||||
|
yield Container(
|
||||||
|
Vertical(DirectoryTree(path), id="tree-view"),
|
||||||
|
Vertical(Static(id="code"), id="code-view"),
|
||||||
|
)
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
def on_directory_tree_file_click(self, event: DirectoryTree.FileClick) -> None:
|
||||||
|
code_view = self.query_one("#code", Static)
|
||||||
|
try:
|
||||||
|
syntax = Syntax.from_path(event.path, line_numbers=True, word_wrap=True)
|
||||||
|
except Exception:
|
||||||
|
code_view.update(Traceback())
|
||||||
|
self.sub_title = "ERROR"
|
||||||
|
else:
|
||||||
|
code_view.update(syntax)
|
||||||
|
self.query_one("#code-view").scroll_home(animate=False)
|
||||||
|
self.sub_title = event.path
|
||||||
|
|
||||||
|
def action_toggle_tree(self) -> None:
|
||||||
|
self.show_tree = not self.show_tree
|
||||||
|
|
||||||
|
|
||||||
|
app = CodeBrowser(css_path="code_browser.css")
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
@@ -65,9 +65,14 @@ class Layout(ABC):
|
|||||||
int: Width of the content.
|
int: Width of the content.
|
||||||
"""
|
"""
|
||||||
width: int | None = None
|
width: int | None = None
|
||||||
|
widget_gutter = widget.gutter.width
|
||||||
for child in widget.displayed_children:
|
for child in widget.displayed_children:
|
||||||
if not child.is_container:
|
if not child.is_container:
|
||||||
child_width = child.get_content_width(container, viewport)
|
child_width = (
|
||||||
|
child.get_content_width(container, viewport)
|
||||||
|
+ widget_gutter
|
||||||
|
+ child.gutter.width
|
||||||
|
)
|
||||||
width = child_width if width is None else max(width, child_width)
|
width = child_width if width is None else max(width, child_width)
|
||||||
if width is None:
|
if width is None:
|
||||||
width = container.width
|
width = container.width
|
||||||
|
|||||||
@@ -544,7 +544,7 @@ class DOMNode(MessagePump):
|
|||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def displayed_children(self) -> list[DOMNode]:
|
def displayed_children(self) -> list[Widget]:
|
||||||
"""The children which don't have display: none set.
|
"""The children which don't have display: none set.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class Container(Widget):
|
|||||||
Container {
|
Container {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,13 @@ from operator import attrgetter
|
|||||||
from typing import TYPE_CHECKING, ClassVar, Collection, Iterable, NamedTuple
|
from typing import TYPE_CHECKING, ClassVar, Collection, Iterable, NamedTuple
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.console import Console, JustifyMethod, RenderableType
|
from rich.console import (
|
||||||
from rich.measure import Measurement
|
Console,
|
||||||
|
ConsoleRenderable,
|
||||||
|
Measurement,
|
||||||
|
JustifyMethod,
|
||||||
|
RenderableType,
|
||||||
|
)
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
from rich.styled import Styled
|
from rich.styled import Styled
|
||||||
@@ -313,17 +318,15 @@ class Widget(DOMNode):
|
|||||||
"""
|
"""
|
||||||
if self.is_container:
|
if self.is_container:
|
||||||
assert self._layout is not None
|
assert self._layout is not None
|
||||||
return (
|
return self._layout.get_content_width(self, container, viewport)
|
||||||
self._layout.get_content_width(self, container, viewport)
|
|
||||||
+ self.scrollbar_size_vertical
|
|
||||||
)
|
|
||||||
|
|
||||||
cache_key = container.width
|
cache_key = container.width
|
||||||
if self._content_width_cache[0] == cache_key:
|
if self._content_width_cache[0] == cache_key:
|
||||||
return self._content_width_cache[1]
|
return self._content_width_cache[1]
|
||||||
|
|
||||||
console = self.app.console
|
console = self.app.console
|
||||||
renderable = self.render()
|
renderable = self.post_render(self.render())
|
||||||
|
|
||||||
measurement = Measurement.get(
|
measurement = Measurement.get(
|
||||||
console,
|
console,
|
||||||
console.options.update_width(container.width),
|
console.options.update_width(container.width),
|
||||||
@@ -509,18 +512,18 @@ class Widget(DOMNode):
|
|||||||
@property
|
@property
|
||||||
def scrollbar_size_vertical(self) -> int:
|
def scrollbar_size_vertical(self) -> int:
|
||||||
"""Get the width used by the *vertical* scrollbar."""
|
"""Get the width used by the *vertical* scrollbar."""
|
||||||
return (
|
styles = self.styles
|
||||||
self.styles.scrollbar_size_vertical if self.show_vertical_scrollbar else 0
|
if styles.scrollbar_gutter == "stable" and styles.overflow_y == "auto":
|
||||||
)
|
return styles.scrollbar_size_vertical
|
||||||
|
return styles.scrollbar_size_vertical if self.show_vertical_scrollbar else 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scrollbar_size_horizontal(self) -> int:
|
def scrollbar_size_horizontal(self) -> int:
|
||||||
"""Get the height used by the *horizontal* scrollbar."""
|
"""Get the height used by the *horizontal* scrollbar."""
|
||||||
return (
|
styles = self.styles
|
||||||
self.styles.scrollbar_size_horizontal
|
if styles.scrollbar_gutter == "stable" and styles.overflow_x == "auto":
|
||||||
if self.show_horizontal_scrollbar
|
return self.styles.scrollbar_size_horizontal
|
||||||
else 0
|
return styles.scrollbar_size_horizontal if self.show_horizontal_scrollbar else 0
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scrollbar_gutter(self) -> Spacing:
|
def scrollbar_gutter(self) -> Spacing:
|
||||||
@@ -1214,7 +1217,7 @@ class Widget(DOMNode):
|
|||||||
if self.descendant_has_focus:
|
if self.descendant_has_focus:
|
||||||
yield "focus-within"
|
yield "focus-within"
|
||||||
|
|
||||||
def post_render(self, renderable: RenderableType) -> RenderableType:
|
def post_render(self, renderable: RenderableType) -> ConsoleRenderable:
|
||||||
"""Applies style attributes to the default renderable.
|
"""Applies style attributes to the default renderable.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -21,14 +21,13 @@ class DirEntry:
|
|||||||
is_dir: bool
|
is_dir: bool
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
|
||||||
class FileClick(Message, bubble=True):
|
|
||||||
def __init__(self, sender: MessageTarget, path: str) -> None:
|
|
||||||
self.path = path
|
|
||||||
super().__init__(sender)
|
|
||||||
|
|
||||||
|
|
||||||
class DirectoryTree(TreeControl[DirEntry]):
|
class DirectoryTree(TreeControl[DirEntry]):
|
||||||
|
@rich.repr.auto
|
||||||
|
class FileClick(Message, bubble=True):
|
||||||
|
def __init__(self, sender: MessageTarget, path: str) -> None:
|
||||||
|
self.path = path
|
||||||
|
super().__init__(sender)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
path: str,
|
path: str,
|
||||||
@@ -112,7 +111,7 @@ class DirectoryTree(TreeControl[DirEntry]):
|
|||||||
) -> None:
|
) -> None:
|
||||||
dir_entry = message.node.data
|
dir_entry = message.node.data
|
||||||
if not dir_entry.is_dir:
|
if not dir_entry.is_dir:
|
||||||
await self.emit(FileClick(self, dir_entry.path))
|
await self.emit(self.FileClick(self, dir_entry.path))
|
||||||
else:
|
else:
|
||||||
if not message.node.loaded:
|
if not message.node.loaded:
|
||||||
await self.load_directory(message.node)
|
await self.load_directory(message.node)
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class HeaderTitle(Widget):
|
|||||||
def render(self) -> Text:
|
def render(self) -> Text:
|
||||||
text = Text(self.text, no_wrap=True, overflow="ellipsis")
|
text = Text(self.text, no_wrap=True, overflow="ellipsis")
|
||||||
if self.sub_text:
|
if self.sub_text:
|
||||||
text.append(f" - {self.sub_text}", "dim")
|
text.append(" — ")
|
||||||
|
text.append(self.sub_text, "dim")
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
@@ -83,8 +84,13 @@ class Header(Widget):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
tall = Reactive(True)
|
||||||
|
|
||||||
DEFAULT_CLASSES = "tall"
|
DEFAULT_CLASSES = "tall"
|
||||||
|
|
||||||
|
def watch_tall(self, tall: bool) -> None:
|
||||||
|
self.set_class(tall, "tall")
|
||||||
|
|
||||||
async def on_click(self, event):
|
async def on_click(self, event):
|
||||||
self.toggle_class("tall")
|
self.toggle_class("tall")
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
|
|
||||||
from rich.protocol import is_renderable
|
from rich.protocol import is_renderable
|
||||||
|
|
||||||
|
from ..reactive import Reactive
|
||||||
from ..errors import RenderError
|
from ..errors import RenderError
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
@@ -49,4 +49,4 @@ class Static(Widget):
|
|||||||
def update(self, renderable: RenderableType) -> None:
|
def update(self, renderable: RenderableType) -> None:
|
||||||
_check_renderable(renderable)
|
_check_renderable(renderable)
|
||||||
self.renderable = renderable
|
self.renderable = renderable
|
||||||
self.refresh()
|
self.refresh(layout=True)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from ..geometry import Region, Size
|
|||||||
from .. import events
|
from .. import events
|
||||||
from ..reactive import Reactive
|
from ..reactive import Reactive
|
||||||
from .._types import MessageTarget
|
from .._types import MessageTarget
|
||||||
from ..widget import Widget
|
from ..widgets import Static
|
||||||
from ..message import Message
|
from ..message import Message
|
||||||
from .. import messages
|
from .. import messages
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ class TreeNode(Generic[NodeDataType]):
|
|||||||
return self._control.render_node(self)
|
return self._control.render_node(self)
|
||||||
|
|
||||||
|
|
||||||
class TreeControl(Generic[NodeDataType], Widget, can_focus=True):
|
class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
TreeControl {
|
TreeControl {
|
||||||
background: $surface;
|
background: $surface;
|
||||||
|
|||||||
Reference in New Issue
Block a user