Source code for urwidgets.hyperlink

from __future__ import annotations

__all__ = ("Hyperlink",)

from typing import Generator, List, Optional, Tuple

import urwid

from .text_embed import DisplayAttribute

# NOTE: Any new "private" attribute of any subclass of an urwid class should be
# prepended with "_uw" to avoid clashes with names used by urwid itself.


ESC = "\033"
OSC = f"{ESC}]"
ST = f"{ESC}\\"
START = f"{OSC}8;id=%d;%s{ST}".encode()
END = f"{OSC}8;;{ST}".encode()

valid_byte_range = range(32, 127)






class HyperlinkCanvas(urwid.Canvas):
    cacheable = False

    _uw_next_id = 0
    _uw_free_ids = set()

    def __init__(self, uri: str, text_canv: urwid.TextCanvas) -> None:
        super().__init__()
        self._uw_text_canv = text_canv
        self._uw_uri = uri.encode()
        self._uw_id = self._uw_get_id()

    def __del__(self):
        __class__._uw_free_ids.add(self._uw_id)

    def cols(self):
        return self._uw_text_canv.cols()

    def content(
        self,
        trim_left: int = 0,
        trim_top: int = 0,
        cols: int | None = None,
        rows: int | None = None,
        attr: DisplayAttribute = None,
    ) -> Generator[List[Tuple[DisplayAttribute, Optional[str], bytes]], None, None]:
        # There can only be one line since wrap="ellipsis" and the text was checked
        # to not contain "\n".
        content_line = next(
            self._uw_text_canv.content(trim_left, trim_top, cols, rows, attr)
        )

        if isinstance(content_line[0][0], _Attr):
            hyperlink_text, *padding = content_line
            link_attr = hyperlink_text[0].attr
            yield [
                (None, "U", START % (self._uw_id, self._uw_uri)),
                (
                    attr.get(link_attr, link_attr) if attr else link_attr,
                    *hyperlink_text[1:],
                ),
                (None, "U", END),
                *padding,  # if any
            ]
        else:  # A trim containing padding only
            yield content_line

    def rows(self):
        return self._uw_text_canv.rows()

    @staticmethod
    def _uw_get_id():
        if __class__._uw_free_ids:
            return __class__._uw_free_ids.pop()
        __class__._uw_next_id += 1

        return __class__._uw_next_id - 1


class _Attr:
    """Wraps a text display attribute to ensure it's always distinguished from those of
    neighbouring text runs.
    """

    def __init__(self, attr: DisplayAttribute):
        self.attr = attr