Skip to content

xnano.components.link

xnano.components.link


Display a focusable link whose destination is available to event hooks.

Classes:

  • Link

    Focusable hyperlink label.

Link(
    content: str | Text | list[str | Text] = "",
    foreground: ColorLike | None = "blue",
    background: ColorLike | None = None,
    modifiers: tuple[CharacterModifier, ...] = (),
    horizontal_align: Alignment | None = None,
    vertical_align: VerticalAlignment | None = None,
    wrap: bool = True,
    input: bool = False,
    placeholder: str | Text | None = None,
    cursor: int | None = None,
    multiline: bool = False,
    rows: int | None = None,
    ansi: bool = False,
    markdown: bool = False,
    language: str | None = None,
    passthrough: Sequence[str] = (),
    mask: str | None = None,
    max_length: int | None = None,
    read_only: bool = False,
    tab_size: int = 4,
    focusable: bool = True,
    fill: bool = False,
    url: str = "",
    underline: bool = True,
    focused_color: ColorLike | None = None,
    visited: bool = False,
    *,
    visible: bool = True,
    z: int = 0,
    fit_content: bool = True
)

Bases: Text

Focusable hyperlink label.

Links are underlined and blue by default. Handle an activation key in a grid hook to open, copy, or otherwise use url.

Example

Link(content="Documentation", url="https://example.com/docs")

Attributes:

Methods:

  • component_post_init

    Validate modes without forcing input=True.

  • compose

    Compose underlined, focus-aware link content.

  • handle_keyboard

    Leave activation keys for application hooks.

  • get_frame

    Optional frame/panel chrome around composed content.

  • get_size

    Return the preferred cell size of this component.

  • before_render

    Called before rendering; returns the effective render area.

  • after_render

    Record the multi-line editor caret so the terminal cursor tracks it.

  • compose_extra_small

    Compose content when the viewport is extra small (< 40 cols).

  • compose_small

    Compose content when the viewport is small (40–79 cols).

  • compose_medium

    Compose content when the viewport is medium (80–119 cols).

  • compose_large

    Compose content when the viewport is large (120–159 cols).

  • compose_extra_large

    Compose content when the viewport is extra large (>= 160 cols).

  • handle_paste

    Insert pasted text at the caret of a multi-line editor.

  • get_terminal_node

    Return composed content for terminal compatibility.

url class-attribute instance-attribute

url: str = ''

Destination address exposed to hooks; never auto-opened.

underline class-attribute instance-attribute

underline: bool = True

When True, compose with the underline modifier.

foreground class-attribute instance-attribute

foreground: ColorLike | None = 'blue'

Default link foreground color.

focused_color class-attribute instance-attribute

focused_color: ColorLike | None = None

Foreground override while this link holds focus.

visited class-attribute instance-attribute

visited: bool = False

Application-maintained visited flag.

focusable class-attribute instance-attribute

focusable: bool = True

Links participate in field focus by default.

value property writable

value: str

Plain label text, falling back to url when empty.

visible class-attribute instance-attribute

visible: bool = dataclasses.field(
    default=True, kw_only=True
)

Whether this component paints at all.

z class-attribute instance-attribute

z: int = dataclasses.field(default=0, kw_only=True)

Stacking order among sibling content.

fit_content class-attribute instance-attribute

fit_content: bool = dataclasses.field(
    default=True, kw_only=True
)

When True, paint at natural size inside a larger slot.

focused property

focused: bool

Whether this component currently holds field focus.

content class-attribute instance-attribute

content: str | Text | list[str | Text] = dataclasses.field(
    default=""
)

Plain string, nested Text, or a list of either.

background class-attribute instance-attribute

background: ColorLike | None = None

Background color.

modifiers class-attribute instance-attribute

modifiers: tuple[CharacterModifier, ...] = ()

Character modifiers such as bold or underline.

horizontal_align class-attribute instance-attribute

horizontal_align: Alignment | None = None

Horizontal alignment at the paragraph level.

vertical_align class-attribute instance-attribute

vertical_align: VerticalAlignment | None = None

Vertical alignment within the painted area.

wrap class-attribute instance-attribute

wrap: bool = True

Whether long lines may wrap.

input class-attribute instance-attribute

input: bool = False

When True on a leaf, the component is an editable field.

placeholder class-attribute instance-attribute

placeholder: str | Text | None = None

Shown when input is empty and unfocused.

cursor class-attribute instance-attribute

cursor: int | None = None

Caret index for single-line input; None means end of string.

multiline class-attribute instance-attribute

multiline: bool = False

When True with input, editing uses CoreTextEditor.

rows class-attribute instance-attribute

rows: int | None = None

Preferred visible height (lines) for a multiline input.

ansi class-attribute instance-attribute

ansi: bool = False

Parse ANSI SGR sequences in leaf content.

markdown class-attribute instance-attribute

markdown: bool = False

Parse leaf content as markdown.

language class-attribute instance-attribute

language: str | None = None

Pygments lexer name for syntax highlighting only.

passthrough class-attribute instance-attribute

passthrough: Sequence[str] = ()

Key bindings this input never captures while focused.

mask class-attribute instance-attribute

mask: str | None = None

Display-only mask character(s); real value is preserved.

max_length class-attribute instance-attribute

max_length: int | None = None

Maximum plain-string length; longer input is clamped.

read_only class-attribute instance-attribute

read_only: bool = False

Reject edits while remaining focusable.

tab_size class-attribute instance-attribute

tab_size: int = 4

Tab width applied to multiline tab insertion and paste.

fill class-attribute instance-attribute

fill: bool = False

Whether background fills the whole slot rather than only the text glyphs. When True short lines still paint the background across the full cell width (no manual right-padding required).

owns_cursor property

owns_cursor: bool

Whether this Text paints its own caret (multi-line editor).

cursor_position property

cursor_position: tuple[int, int] | None

Absolute caret cell for the terminal cursor, set during paint.

Reported only for a focused multi-line editor (a single-line input paints its own caret inline). None otherwise, which hides the hardware cursor.

component_post_init

component_post_init() -> None

Validate modes without forcing input=True.

Source code in xnano/components/link.py
def component_post_init(self) -> None:
    """Validate modes without forcing ``input=True``."""
    super().component_post_init()

compose

compose(
    ctx: ComponentRenderContext[Any],
) -> TextBlock | Any

Compose underlined, focus-aware link content.

Parameters:

Returns:

  • TextBlock | Any

    Styled TextBlock (or nested content) for this link.

Source code in xnano/components/link.py
def compose(self, ctx: ComponentRenderContext[Any]) -> TextBlock | Any:
    """Compose underlined, focus-aware link content.

    Args:
        ctx: Render-time scope for this paint.

    Returns:
        Styled ``TextBlock`` (or nested content) for this link.
    """
    modifiers = tuple(self.modifiers)
    if self.underline and "underline" not in modifiers:
        modifiers = modifiers + ("underline",)

    color = self.foreground
    if self.focused and self.focused_color is not None:
        color = self.focused_color
    elif self.visited and self.focused_color is None:
        # Mild visited cue when no focused override is set.
        if color == "blue":
            color = "magenta"

    # Temporarily apply compose styles without mutating live state
    # beyond the frame: build a plain block from the label.
    if isinstance(self.content, str):
        label = self.content if self.content else self.url
        return TextBlock.from_plain(
            label,
            foreground=color,
            background=self.background,
            modifiers=modifiers,
            horizontal_align=self.horizontal_align,
            vertical_align=self.vertical_align,
            wrap=self.wrap,
            z=self.z,
            visible=self.visible,
        )

    # Nested content: style via a short-lived attribute swap.
    previous_modifiers = self.modifiers
    previous_color = self.foreground
    object.__setattr__(self, "modifiers", modifiers)
    object.__setattr__(self, "foreground", color)
    try:
        return Text.compose(self, ctx)
    finally:
        object.__setattr__(self, "modifiers", previous_modifiers)
        object.__setattr__(self, "foreground", previous_color)

handle_keyboard

handle_keyboard(keyboard: 'KeyboardEventData') -> bool

Leave activation keys for application hooks.

Parameters:

  • keyboard ('KeyboardEventData') –

    The keyboard event payload.

Returns:

  • bool

    Always False so enter/space and other keys bubble.

Source code in xnano/components/link.py
def handle_keyboard(self, keyboard: "KeyboardEventData") -> bool:
    """Leave activation keys for application hooks.

    Args:
        keyboard: The keyboard event payload.

    Returns:
        Always ``False`` so enter/space and other keys bubble.
    """
    if self.passthrough and keyboard.matches(*self.passthrough):
        return False
    # Never consume activation or editing keys on a link.
    return False

get_frame

get_frame() -> Any | None

Optional frame/panel chrome around composed content.

Source code in xnano/components/component.py
def get_frame(self) -> Any | None:
    """Optional frame/panel chrome around composed content."""
    return None

get_size

get_size(ctx: ComponentRenderContext[StateT]) -> Size

Return the preferred cell size of this component.

Source code in xnano/components/component.py
def get_size(self, ctx: ComponentRenderContext[StateT]) -> Size:
    """Return the preferred cell size of this component."""
    return Size(width=0, height=0)

before_render

before_render(
    ctx: ComponentRenderContext[StateT], area: "Area"
) -> "Area"

Called before rendering; returns the effective render area.

Source code in xnano/components/component.py
def before_render(
    self,
    ctx: ComponentRenderContext[StateT],
    area: "Area",
) -> "Area":
    """Called before rendering; returns the effective render area."""
    return area

after_render

after_render(
    ctx: "ComponentRenderContext[Any]", area: "Area"
) -> None

Record the multi-line editor caret so the terminal cursor tracks it.

Source code in xnano/components/text.py
def after_render(
    self,
    ctx: "ComponentRenderContext[Any]",
    area: "Area",
) -> None:
    """Record the multi-line editor caret so the terminal cursor tracks it."""
    if not (self._input_focused and self._editor is not None):
        self._cursor_position = None
        return
    row, column = self._editor.cursor()
    # ponytail: no soft-wrap/scroll accounting; clamps to the slot, which
    # is exact for a note-sized editor. Add offsets if the body scrolls.
    x = area.x + max(0, min(column, max(0, area.width - 1)))
    y = area.y + max(0, min(row, max(0, area.height - 1)))
    self._cursor_position = (x, y)

compose_extra_small

compose_extra_small(
    ctx: ComponentRenderContext[StateT],
) -> Content | None

Compose content when the viewport is extra small (< 40 cols).

Optional responsive counterpart to :meth:compose. When overridden, it is used instead of compose while the window is in this size tier. Overriding any compose_* variant opts the component into breakpoint dispatch; a component that overrides none pays no per-frame cost.

Source code in xnano/components/component.py
@responsive_noop
def compose_extra_small(
    self, ctx: ComponentRenderContext[StateT]
) -> Content | None:
    """Compose content when the viewport is extra small (< 40 cols).

    Optional responsive counterpart to :meth:`compose`. When
    overridden, it is used *instead of* ``compose`` while the window
    is in this size tier. Overriding any ``compose_*`` variant opts the
    component into breakpoint dispatch; a component that overrides none
    pays no per-frame cost.
    """
    return None

compose_small

compose_small(
    ctx: ComponentRenderContext[StateT],
) -> Content | None

Compose content when the viewport is small (40–79 cols).

See :meth:compose_extra_small.

Source code in xnano/components/component.py
@responsive_noop
def compose_small(
    self, ctx: ComponentRenderContext[StateT]
) -> Content | None:
    """Compose content when the viewport is small (40–79 cols).

    See :meth:`compose_extra_small`.
    """
    return None

compose_medium

compose_medium(
    ctx: ComponentRenderContext[StateT],
) -> Content | None

Compose content when the viewport is medium (80–119 cols).

See :meth:compose_extra_small.

Source code in xnano/components/component.py
@responsive_noop
def compose_medium(
    self, ctx: ComponentRenderContext[StateT]
) -> Content | None:
    """Compose content when the viewport is medium (80–119 cols).

    See :meth:`compose_extra_small`.
    """
    return None

compose_large

compose_large(
    ctx: ComponentRenderContext[StateT],
) -> Content | None

Compose content when the viewport is large (120–159 cols).

See :meth:compose_extra_small.

Source code in xnano/components/component.py
@responsive_noop
def compose_large(
    self, ctx: ComponentRenderContext[StateT]
) -> Content | None:
    """Compose content when the viewport is large (120–159 cols).

    See :meth:`compose_extra_small`.
    """
    return None

compose_extra_large

compose_extra_large(
    ctx: ComponentRenderContext[StateT],
) -> Content | None

Compose content when the viewport is extra large (>= 160 cols).

See :meth:compose_extra_small.

Source code in xnano/components/component.py
@responsive_noop
def compose_extra_large(
    self, ctx: ComponentRenderContext[StateT]
) -> Content | None:
    """Compose content when the viewport is extra large (>= 160 cols).

    See :meth:`compose_extra_small`.
    """
    return None

handle_paste

handle_paste(text: str) -> bool

Insert pasted text at the caret of a multi-line editor.

Parameters:

  • text (str) –

    The pasted clipboard text.

Returns:

  • bool

    True when the paste was consumed by the native editor.

Source code in xnano/components/text.py
def handle_paste(self, text: str) -> bool:
    """Insert pasted text at the caret of a multi-line editor.

    Args:
        text: The pasted clipboard text.

    Returns:
        ``True`` when the paste was consumed by the native editor.
    """
    if self._editor is None:
        return False
    if self.read_only:
        return True
    text = self._clamp_text(text)
    if self.max_length is not None:
        remaining = self.max_length - len(self._editor.text())
        if remaining <= 0:
            return True
        text = text[:remaining]
    self._editor.insert_text(text)
    object.__setattr__(self, "content", self._editor.text())
    return True

get_terminal_node

get_terminal_node(
    ctx: ComponentRenderContext[Any],
) -> TextBlock | Native | Panel | None

Return composed content for terminal compatibility.

Parameters:

Returns:

Source code in xnano/components/text.py
def get_terminal_node(
    self, ctx: ComponentRenderContext[Any]
) -> TextBlock | Native | Panel | None:
    """Return composed content for terminal compatibility.

    Args:
        ctx: Render-time scope for this paint.

    Returns:
        The same interface-neutral content as ``compose``.
    """
    return self.compose(ctx)