"""Renderer for markdown content."""
from __future__ import annotations
from rich.console import Console, RenderableType, ConsoleOptions, RenderResult
from rich.markdown import Markdown
from rich.segment import Segment
from rich.style import Style
from rich.theme import Theme as RichTheme
from clients.terminal.tui.themes import ThemeRegistry, get_active_theme
from .base import ContentRenderer
def _theme_aware_code_theme(theme_name: str) -> str:
"""Resolve the Pygments code style for a Navi theme name.
The single source of truth is ``Theme.code_theme``; this is a thin
resolver for callers that only hold the theme name (rich's ``Markdown``
takes a style *string*, not a Theme). All syntax highlighting in the TUI
flows through here so the whole UI shares one code colour scheme.
"""
return ThemeRegistry.get(theme_name).code_theme
class ThemedMarkdownRenderable:
"""Wrap Markdown and render it with Navi theme colors applied."""
def __init__(self, markdown: Markdown, theme_name: str) -> None:
self._markdown = markdown
self._theme_name = theme_name
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
theme = get_active_theme()
# Render at the *available* width (options.max_width), not the physical
# console width. Textual passes a console whose .width is the terminal
# width (e.g. 80), but the panel the markdown lives in is narrower —
# options.max_width holds the real available columns (panel inner width
# minus borders/padding). Using console.width here made rich wrap to the
# terminal width while the visible area was narrower, so the right side
# of every long line vanished off the panel's right edge. Fall back to
# console.width only if max_width is unset.
themed_console = Console(
width=options.max_width or console.width,
color_system=console.color_system,
theme=RichTheme(theme.rich_theme_styles()),
force_terminal=True,
)
link_color = Style.parse(theme.link.hex).color
# Stream segments straight through instead of materializing them into a
# list first — every paint (scroll, resize, reading-mode toggle) would
# otherwise allocate the full segment list + walk it twice (once to
# build, once to rewrite links). Inline the link rewrite in the loop.
for segment in themed_console.render(self._markdown):
style = segment.style
if style and style.link and link_color is not None:
yield Segment(
segment.text,
Style(color=link_color, underline=True, link=style.link),
)
else:
yield segment
class MarkdownRenderer(ContentRenderer):
"""Render markdown text with syntax highlighting."""
def accepts(self, msg: dict) -> bool:
return msg.get("type") == "markdown"
def render(self, msg: dict) -> RenderableType:
theme = get_active_theme()
text = msg.get("content", "")
code_theme = _theme_aware_code_theme(theme.name)
md = Markdown(
text,
code_theme=code_theme,
inline_code_theme=code_theme,
)
return ThemedMarkdownRenderable(md, theme.name)