Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
edit.py725 linesDownload Raw Back to widget
1from __future__ import annotations2 3import string4import typing5 6from urwid import text_layout7from urwid.canvas import CompositeCanvas8from urwid.command_map import Command9from urwid.split_repr import remove_defaults10from urwid.str_util import is_wide_char, move_next_char, move_prev_char11from urwid.util import decompose_tagmarkup12 13from .constants import Align, Sizing, WrapMode14from .text import Text, TextError15 16if typing.TYPE_CHECKING:17    from collections.abc import Hashable18 19    from typing_extensions import Literal20 21    from urwid.canvas import TextCanvas22 23 24class EditError(TextError):25    pass26 27 28class Edit(Text):29    """30    Text editing widget implements cursor movement, text insertion and31    deletion.  A caption may prefix the editing area.  Uses text class32    for text layout.33 34    Users of this class may listen for ``"change"`` or ``"postchange"``35    events.  See :func:``connect_signal``.36 37    * ``"change"`` is sent just before the value of edit_text changes.38      It receives the new text as an argument.  Note that ``"change"`` cannot39      change the text in question as edit_text changes the text afterwards.40    * ``"postchange"`` is sent after the value of edit_text changes.41      It receives the old value of the text as an argument and thus is42      appropriate for changing the text.  It is possible for a ``"postchange"``43      event handler to get into a loop of changing the text and then being44      called when the event is re-emitted.  It is up to the event45      handler to guard against this case (for instance, by not changing the46      text if it is signaled for text that it has already changed once).47    """48 49    _sizing = frozenset([Sizing.FLOW])50    _selectable = True51    ignore_focus = False52    # (this variable is picked up by the MetaSignals metaclass)53    signals: typing.ClassVar[list[str]] = ["change", "postchange"]54 55    def valid_char(self, ch: str) -> bool:56        """57        Filter for text that may be entered into this widget by the user58 59        :param ch: character to be inserted60        :type ch: str61 62        This implementation returns True for all printable characters.63        """64        return is_wide_char(ch, 0) or (len(ch) == 1 and ord(ch) >= 32)65 66    def __init__(67        self,68        caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]] = "",69        edit_text: str = "",70        multiline: bool = False,71        align: Literal["left", "center", "right"] | Align = Align.LEFT,72        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,73        allow_tab: bool = False,74        edit_pos: int | None = None,75        layout: text_layout.TextLayout = None,76        mask: str | None = None,77    ) -> None:78        """79        :param caption: markup for caption preceding edit_text, see80                        :class:`Text` for description of text markup.81        :type caption: text markup82        :param edit_text: initial text for editing, type (bytes or unicode)83                          must match the text in the caption84        :type edit_text: bytes or unicode85        :param multiline: True: 'enter' inserts newline  False: return it86        :type multiline: bool87        :param align: typically 'left', 'center' or 'right'88        :type align: text alignment mode89        :param wrap: typically 'space', 'any' or 'clip'90        :type wrap: text wrapping mode91        :param allow_tab: True: 'tab' inserts 1-8 spaces  False: return it92        :type allow_tab: bool93        :param edit_pos: initial position for cursor, None:end of edit_text94        :type edit_pos: int95        :param layout: defaults to a shared :class:`StandardTextLayout` instance96        :type layout: text layout instance97        :param mask: hide text entered with this character, None:disable mask98        :type mask: bytes or unicode99 100        >>> Edit()101        <Edit selectable flow widget '' edit_pos=0>102        >>> Edit("Y/n? ", "yes")103        <Edit selectable flow widget 'yes' caption='Y/n? ' edit_pos=3>104        >>> Edit("Name ", "Smith", edit_pos=1)105        <Edit selectable flow widget 'Smith' caption='Name ' edit_pos=1>106        >>> Edit("", "3.14", align="right")107        <Edit selectable flow widget '3.14' align='right' edit_pos=4>108        """109 110        super().__init__("", align, wrap, layout)111        self.multiline = multiline112        self.allow_tab = allow_tab113        self._edit_pos = 0114        self._caption, self._attrib = decompose_tagmarkup(caption)115        self._edit_text = ""116        self.highlight: tuple[int, int] | None = None117        self.set_edit_text(edit_text)118        if edit_pos is None:119            edit_pos = len(edit_text)120        self.set_edit_pos(edit_pos)121        self.set_mask(mask)122        self._shift_view_to_cursor = False123 124    def _repr_words(self) -> list[str]:125        return (126            super()._repr_words()[:-1]127            + [repr(self._edit_text)]128            + [f"caption={self._caption!r}"] * bool(self._caption)129            + ["multiline"] * (self.multiline is True)130        )131 132    def _repr_attrs(self) -> dict[str, typing.Any]:133        attrs = {**super()._repr_attrs(), "edit_pos": self._edit_pos}134        return remove_defaults(attrs, Edit.__init__)135 136    def get_text(self) -> tuple[str | bytes, list[tuple[Hashable, int]]]:137        """138        Returns ``(text, display attributes)``. See :meth:`Text.get_text`139        for details.140 141        Text returned includes the caption and edit_text, possibly masked.142 143        >>> Edit("What? ", "oh, nothing.").get_text()144        ('What? oh, nothing.', [])145        >>> Edit(("bright", "user@host:~$ "), "ls").get_text()146        ('user@host:~$ ls', [('bright', 13)])147        >>> Edit("password:", "seekrit", mask="*").get_text()148        ('password:*******', [])149        """150 151        if self._mask is None:152            return self._caption + self._edit_text, self._attrib153 154        return self._caption + (self._mask * len(self._edit_text)), self._attrib155 156    def set_text(self, markup: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]) -> None:157        """158        Not supported by Edit widget.159 160        >>> Edit().set_text("test")161        Traceback (most recent call last):162        EditError: set_text() not supported.  Use set_caption() or set_edit_text() instead.163        """164        # FIXME: this smells. reimplement Edit as a WidgetWrap subclass to165        # clean this up166 167        # hack to let Text.__init__() work168        if not hasattr(self, "_text") and markup == "":  # noqa: PLC1901,RUF100169            self._text = None170            return171 172        raise EditError("set_text() not supported.  Use set_caption() or set_edit_text() instead.")173 174    def get_pref_col(self, size: tuple[int]) -> int:175        """176        Return the preferred column for the cursor, or the177        current cursor x value.  May also return ``'left'`` or ``'right'``178        to indicate the leftmost or rightmost column available.179 180        This method is used internally and by other widgets when181        moving the cursor up or down between widgets so that the182        column selected is one that the user would expect.183 184        >>> size = (10,)185        >>> Edit().get_pref_col(size)186        0187        >>> e = Edit("", "word")188        >>> e.get_pref_col(size)189        4190        >>> e.keypress(size, "left")191        >>> e.get_pref_col(size)192        3193        >>> e.keypress(size, "end")194        >>> e.get_pref_col(size)195        <Align.RIGHT: 'right'>196        >>> e = Edit("", "2\\nwords")197        >>> e.keypress(size, "left")198        >>> e.keypress(size, "up")199        >>> e.get_pref_col(size)200        4201        >>> e.keypress(size, "left")202        >>> e.get_pref_col(size)203        0204        """205        (maxcol,) = size206        pref_col, then_maxcol = self.pref_col_maxcol207        if then_maxcol != maxcol:208            return self.get_cursor_coords((maxcol,))[0]209 210        return pref_col211 212    def set_caption(self, caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]) -> None:213        """214        Set the caption markup for this widget.215 216        :param caption: markup for caption preceding edit_text, see217                        :meth:`Text.__init__` for description of text markup.218 219        >>> e = Edit("")220        >>> e.set_caption("cap1")221        >>> print(e.caption)222        cap1223        >>> e.set_caption(("bold", "cap2"))224        >>> print(e.caption)225        cap2226        >>> e.attrib227        [('bold', 4)]228        >>> e.caption = "cap3"  # not supported because caption stores text but set_caption() takes markup229        Traceback (most recent call last):230        AttributeError: can't set attribute231        """232        self._caption, self._attrib = decompose_tagmarkup(caption)233        self._invalidate()234 235    @property236    def caption(self) -> str:237        """238        Read-only property returning the caption for this widget.239        """240        return self._caption241 242    def set_edit_pos(self, pos: int) -> None:243        """244        Set the cursor position with a self.edit_text offset.245        Clips pos to [0, len(edit_text)].246 247        :param pos: cursor position248        :type pos: int249 250        >>> e = Edit("", "word")251        >>> e.edit_pos252        4253        >>> e.set_edit_pos(2)254        >>> e.edit_pos255        2256        >>> e.edit_pos = -1  # Urwid 0.9.9 or later257        >>> e.edit_pos258        0259        >>> e.edit_pos = 20260        >>> e.edit_pos261        4262        """263        pos = min(max(pos, 0), len(self._edit_text))264        self.highlight = None265        self.pref_col_maxcol = None, None266        self._edit_pos = pos267        self._invalidate()268 269    edit_pos = property(270        lambda self: self._edit_pos,271        set_edit_pos,272        doc="""273        Property controlling the edit position for this widget.274        """,275    )276 277    def set_mask(self, mask: str | None) -> None:278        """279        Set the character for masking text away.280 281        :param mask: hide text entered with this character, None:disable mask282        :type mask: bytes or unicode283        """284 285        self._mask = mask286        self._invalidate()287 288    def set_edit_text(self, text: str) -> None:289        """290        Set the edit text for this widget.291 292        :param text: text for editing, type (bytes or unicode)293                     must match the text in the caption294        :type text: bytes or unicode295 296        >>> e = Edit()297        >>> e.set_edit_text("yes")298        >>> print(e.edit_text)299        yes300        >>> e301        <Edit selectable flow widget 'yes' edit_pos=0>302        >>> e.edit_text = "no"  # Urwid 0.9.9 or later303        >>> print(e.edit_text)304        no305        """306        text = self._normalize_to_caption(text)307        self.highlight = None308        self._emit("change", text)309        old_text = self._edit_text310        self._edit_text = text311        self.edit_pos = min(self.edit_pos, len(text))312 313        self._emit("postchange", old_text)314        self._invalidate()315 316    def get_edit_text(self) -> str:317        """318        Return the edit text for this widget.319 320        >>> e = Edit("What? ", "oh, nothing.")321        >>> print(e.get_edit_text())322        oh, nothing.323        >>> print(e.edit_text)324        oh, nothing.325        """326        return self._edit_text327 328    edit_text = property(329        get_edit_text,330        set_edit_text,331        doc="""332        Property controlling the edit text for this widget.333        """,334    )335 336    def insert_text(self, text: str) -> None:337        """338        Insert text at the cursor position and update cursor.339        This method is used by the keypress() method when inserting340        one or more characters into edit_text.341 342        :param text: text for inserting, type (bytes or unicode)343                     must match the text in the caption344        :type text: bytes or unicode345 346        >>> e = Edit("", "42")347        >>> e.insert_text(".5")348        >>> e349        <Edit selectable flow widget '42.5' edit_pos=4>350        >>> e.set_edit_pos(2)351        >>> e.insert_text("a")352        >>> print(e.edit_text)353        42a.5354        """355        text = self._normalize_to_caption(text)356        result_text, result_pos = self.insert_text_result(text)357        self.set_edit_text(result_text)358        self.set_edit_pos(result_pos)359        self.highlight = None360 361    def _normalize_to_caption(self, text: str | bytes) -> str | bytes:362        """Return text converted to the same type as self.caption (bytes or unicode)"""363        tu = isinstance(text, str)364        cu = isinstance(self._caption, str)365        if tu == cu:366            return text367        if tu:368            return text.encode("ascii")  # follow python2's implicit conversion369        return text.decode("ascii")370 371    def insert_text_result(self, text: str) -> tuple[str | bytes, int]:372        """373        Return result of insert_text(text) without actually performing the374        insertion.  Handy for pre-validation.375 376        :param text: text for inserting, type (bytes or unicode)377                     must match the text in the caption378        :type text: bytes or unicode379        """380 381        # if there's highlighted text, it'll get replaced by the new text382        text = self._normalize_to_caption(text)383        if self.highlight:384            start, stop = self.highlight385            btext, etext = self.edit_text[:start], self.edit_text[stop:]386            result_text = btext + etext387            result_pos = start388        else:389            result_text = self.edit_text390            result_pos = self.edit_pos391 392        try:393            result_text = result_text[:result_pos] + text + result_text[result_pos:]394        except (IndexError, TypeError) as exc:395            raise ValueError(repr((self.edit_text, result_text, text))).with_traceback(exc.__traceback__) from exc396 397        result_pos += len(text)398        return (result_text, result_pos)399 400    def keypress(401        self,402        size: tuple[int],  # type: ignore[override]403        key: str,404    ) -> str | None:405        """406        Handle editing keystrokes, return others.407 408        >>> e, size = Edit(), (20,)409        >>> e.keypress(size, "x")410        >>> e.keypress(size, "left")411        >>> e.keypress(size, "1")412        >>> print(e.edit_text)413        1x414        >>> e.keypress(size, "backspace")415        >>> e.keypress(size, "end")416        >>> e.keypress(size, "2")417        >>> print(e.edit_text)418        x2419        >>> e.keypress(size, "shift f1")420        'shift f1'421        """422        pos = self.edit_pos423        if self.valid_char(key):424            if isinstance(key, str) and not isinstance(self._caption, str):425                key = key.encode("utf-8")426            self.insert_text(key)427            return None428 429        if key == "tab" and self.allow_tab:430            key = " " * (8 - (self.edit_pos % 8))431            self.insert_text(key)432            return None433 434        if key == "enter" and self.multiline:435            key = "\n"436            self.insert_text(key)437            return None438 439        if self._command_map[key] == Command.LEFT:440            if pos == 0:441                return key442            pos = move_prev_char(self.edit_text, 0, pos)443            self.set_edit_pos(pos)444            return None445 446        if self._command_map[key] == Command.RIGHT:447            if pos >= len(self.edit_text):448                return key449            pos = move_next_char(self.edit_text, pos, len(self.edit_text))450            self.set_edit_pos(pos)451            return None452 453        if self._command_map[key] in {Command.UP, Command.DOWN}:454            self.highlight = None455 456            _x, y = self.get_cursor_coords(size)457            pref_col = self.get_pref_col(size)458            if pref_col is None:459                raise ValueError(pref_col)460 461            # if pref_col is None:462            #    pref_col = x463 464            if self._command_map[key] == Command.UP:465                y -= 1466            else:467                y += 1468 469            if not self.move_cursor_to_coords(size, pref_col, y):470                return key471            return None472 473        if key == "backspace":474            self.pref_col_maxcol = None, None475            if not self._delete_highlighted():476                if pos == 0:477                    return key478                pos = move_prev_char(self.edit_text, 0, pos)479                self.set_edit_text(self.edit_text[:pos] + self.edit_text[self.edit_pos :])480                self.set_edit_pos(pos)481                return None482            return None483 484        if key == "delete":485            self.pref_col_maxcol = None, None486            if not self._delete_highlighted():487                if pos >= len(self.edit_text):488                    return key489                pos = move_next_char(self.edit_text, pos, len(self.edit_text))490                self.set_edit_text(self.edit_text[: self.edit_pos] + self.edit_text[pos:])491                return None492            return None493 494        if self._command_map[key] in {Command.MAX_LEFT, Command.MAX_RIGHT}:495            self.highlight = None496            self.pref_col_maxcol = None, None497 498            _x, y = self.get_cursor_coords(size)499 500            if self._command_map[key] == Command.MAX_LEFT:501                self.move_cursor_to_coords(size, Align.LEFT, y)502            else:503                self.move_cursor_to_coords(size, Align.RIGHT, y)504            return None505 506        # key wasn't handled507        return key508 509    def move_cursor_to_coords(510        self,511        size: tuple[int],512        x: int | Literal[Align.LEFT, Align.RIGHT],513        y: int,514    ) -> bool:515        """516        Set the cursor position with (x,y) coordinates.517        Returns True if move succeeded, False otherwise.518 519        >>> size = (10,)520        >>> e = Edit("", "edit\\ntext")521        >>> e.move_cursor_to_coords(size, 5, 0)522        True523        >>> e.edit_pos524        4525        >>> e.move_cursor_to_coords(size, 5, 3)526        False527        >>> e.move_cursor_to_coords(size, 0, 1)528        True529        >>> e.edit_pos530        5531        """532        (maxcol,) = size533        trans = self.get_line_translation(maxcol)534        _top_x, top_y = self.position_coords(maxcol, 0)535        if y < top_y or y >= len(trans):536            return False537 538        pos = text_layout.calc_pos(self.get_text()[0], trans, x, y)539        e_pos = min(max(pos - len(self.caption), 0), len(self.edit_text))540        self.edit_pos = e_pos541        self.pref_col_maxcol = x, maxcol542        self._invalidate()543        return True544 545    def mouse_event(546        self,547        size: tuple[int],  # type: ignore[override]548        event: str,549        button: int,550        col: int,551        row: int,552        focus: bool,553    ) -> bool | None:554        """555        Move the cursor to the location clicked for button 1.556 557        >>> size = (20,)558        >>> e = Edit("", "words here")559        >>> e.mouse_event(size, "mouse press", 1, 2, 0, True)560        True561        >>> e.edit_pos562        2563        """564        if button == 1:565            return self.move_cursor_to_coords(size, col, row)566        return False567 568    def _delete_highlighted(self) -> bool:569        """570        Delete all highlighted text and update cursor position, if any571        text is highlighted.572        """573        if not self.highlight:574            return False575        start, stop = self.highlight576        btext, etext = self.edit_text[:start], self.edit_text[stop:]577        self.set_edit_text(btext + etext)578        self.edit_pos = start579        self.highlight = None580        return True581 582    def render(583        self,584        size: tuple[int],  # type: ignore[override]585        focus: bool = False,586    ) -> TextCanvas | CompositeCanvas:587        """588        Render edit widget and return canvas.  Include cursor when in589        focus.590 591        >>> edit = Edit("? ", "yes")592        >>> c = edit.render((10,), focus=True)593        >>> c.text594        [b'? yes     ']595        >>> c.cursor596        (5, 0)597        """598        self._shift_view_to_cursor = bool(focus)599 600        canv: TextCanvas | CompositeCanvas = super().render(size, focus)601        if focus:602            canv = CompositeCanvas(canv)603            canv.cursor = self.get_cursor_coords(size)604 605        # .. will need to FIXME if I want highlight to work again606        # if self.highlight:607        #    hstart, hstop = self.highlight_coords()608        #    d.coords['highlight'] = [ hstart, hstop ]609        return canv610 611    def get_line_translation(612        self,613        maxcol: int,614        ta: tuple[str | bytes, list[tuple[Hashable, int]]] | None = None,615    ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:616        trans = super().get_line_translation(maxcol, ta)617        if not self._shift_view_to_cursor:618            return trans619 620        text, _ignore = self.get_text()621        x, y = text_layout.calc_coords(text, trans, self.edit_pos + len(self.caption))622        if x < 0:623            return [624                *trans[:y],625                *[text_layout.shift_line(trans[y], -x)],626                *trans[y + 1 :],627            ]628 629        if x >= maxcol:630            return [631                *trans[:y],632                *[text_layout.shift_line(trans[y], -(x - maxcol + 1))],633                *trans[y + 1 :],634            ]635 636        return trans637 638    def get_cursor_coords(self, size: tuple[int]) -> tuple[int, int]:639        """640        Return the (*x*, *y*) coordinates of cursor within widget.641 642        >>> Edit("? ", "yes").get_cursor_coords((10,))643        (5, 0)644        """645        (maxcol,) = size646 647        self._shift_view_to_cursor = True648        return self.position_coords(maxcol, self.edit_pos)649 650    def position_coords(self, maxcol: int, pos: int) -> tuple[int, int]:651        """652        Return (*x*, *y*) coordinates for an offset into self.edit_text.653        """654 655        p = pos + len(self.caption)656        trans = self.get_line_translation(maxcol)657        x, y = text_layout.calc_coords(self.get_text()[0], trans, p)658        return x, y659 660 661class IntEdit(Edit):662    """Edit widget for integer values"""663 664    def valid_char(self, ch: str) -> bool:665        """666        Return true for decimal digits.667        """668        return len(ch) == 1 and ch in string.digits669 670    def __init__(self, caption="", default: int | str | None = None) -> None:671        """672        caption -- caption markup673        default -- default edit value674 675        >>> IntEdit("", 42)676        <IntEdit selectable flow widget '42' edit_pos=2>677        """678        if default is not None:679            val = str(default)680        else:681            val = ""682        super().__init__(caption, val)683 684    def keypress(685        self,686        size: tuple[int],  # type: ignore[override]687        key: str,688    ) -> str | None:689        """690        Handle editing keystrokes.  Remove leading zeros.691 692        >>> e, size = IntEdit("", 5002), (10,)693        >>> e.keypress(size, "home")694        >>> e.keypress(size, "delete")695        >>> print(e.edit_text)696        002697        >>> e.keypress(size, "end")698        >>> print(e.edit_text)699        2700        """701        if unhandled := super().keypress(size, key):702            return unhandled703 704        # trim leading zeros705        while self.edit_pos > 0 and self.edit_text[:1] == "0":706            self.set_edit_pos(self.edit_pos - 1)707            self.set_edit_text(self.edit_text[1:])708 709        return None710 711    def value(self) -> int:712        """713        Return the numeric value of self.edit_text.714 715        >>> e, size = IntEdit(), (10,)716        >>> e.keypress(size, "5")717        >>> e.keypress(size, "1")718        >>> e.value() == 51719        True720        """721        if self.edit_text:722            return int(self.edit_text)723 724        return 0725 
codekingpro/portable-devtools · Team Ai