dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

draft_ui.py (6798B)


      1 from typing import Optional
      2 import re
      3 
      4 from talon.experimental.textarea import (
      5     TextArea,
      6     Span,
      7     DarkThemeLabels,
      8     LightThemeLabels
      9 )
     10 
     11 
     12 word_matcher = re.compile(r"([^\s]+)(\s*)")
     13 def calculate_text_anchors(text, cursor_position, anchor_labels=None):
     14     """
     15     Produces an iterator of (anchor, start_word_index, end_word_index, last_space_index)
     16     tuples from the given text. Each tuple indicates a particular point you may want to 
     17     reference when editing along with some useful ranges you may want to operate on.
     18 
     19     - text is the text you want to process.
     20     - cursor_position is the current position of the cursor, anchors will be placed around
     21       this.
     22     - anchor_labels is a list of characters you want to use for your labels.
     23     - *index is just a character offset from the start of the string (e.g. the first character is at index 0)
     24     - end_word_index is the index of the character after the last one included in the
     25       anchor. That is, you can use it with a slice directly like [start:end]
     26     - anchor is a short piece of text you can use to identify it (e.g. 'a', or '1').
     27     """
     28     anchor_labels = anchor_labels or "abcdefghijklmnopqrstuvwxyz"
     29 
     30     if len(text) == 0:
     31         return []
     32 
     33     # Find all the word spans
     34     matches = []
     35     cursor_idx = None
     36     for match in word_matcher.finditer(text):
     37         matches.append((
     38             match.start(),
     39             match.end() - len(match.group(2)),
     40             match.end()
     41         ))
     42         if matches[-1][0] <= cursor_position and matches[-1][2] >= cursor_position:
     43             cursor_idx = len(matches) - 1
     44 
     45     # Now work out what range of those matches are getting an anchor. The aim is
     46     # to centre the anchors around the cursor position, but also to use all the
     47     # anchors.
     48     anchors_before_cursor = len(anchor_labels) // 2
     49     anchor_start_idx = max(0, cursor_idx - anchors_before_cursor)
     50     anchor_end_idx = min(len(matches), anchor_start_idx + len(anchor_labels))
     51     anchor_start_idx = max(0, anchor_end_idx - len(anchor_labels))
     52 
     53     # Now add anchors to the selected matches
     54     for i, anchor in zip(range(anchor_start_idx, anchor_end_idx), anchor_labels):
     55         word_start, word_end, whitespace_end = matches[i]
     56         yield (
     57             anchor,
     58             word_start,
     59             word_end,
     60             whitespace_end
     61         )
     62 
     63 
     64 class DraftManager:
     65     """
     66     API to the draft window
     67     """
     68 
     69     def __init__(self):
     70         self.area = TextArea()
     71         self.area.title = "Talon Draft"
     72         self.area.value = ""
     73         self.area.register("label", self._update_labels)
     74         self.set_styling()
     75 
     76     def set_styling(
     77         self,
     78         theme="dark",
     79         text_size=20,
     80         label_size=20,
     81         label_color=None
     82     ):
     83         """
     84         Allow settings the style of the draft window. Will dynamically
     85         update the style based on the passed in parameters.
     86         """
     87 
     88         area_theme = DarkThemeLabels if theme == "dark" else LightThemeLabels
     89         theme_changes = {
     90             "text_size": text_size,
     91             "label_size": label_size,
     92         }
     93         if label_color is not None:
     94             theme_changes["label"] = label_color
     95         self.area.theme = area_theme(**theme_changes)
     96 
     97     def show(self, text: Optional[str] = None):
     98         """
     99         Show the window. If text is None then keep the old contents,
    100         otherwise set the text to the given value.
    101         """
    102 
    103         if text is not None:
    104             self.area.value = text
    105         self.area.show()
    106 
    107     def hide(self):
    108         """
    109         Hide the window.
    110         """
    111 
    112         self.area.hide()
    113 
    114     def get_text(self) -> str:
    115         """
    116         Gets the context of the text area
    117         """
    118 
    119         return self.area.value
    120 
    121     def get_rect(self) -> "talon.types.Rect":
    122         """
    123         Get the Rect for the window
    124         """
    125 
    126         return self.area.rect
    127 
    128     def reposition(
    129         self,
    130         xpos: Optional[int] = None,
    131         ypos: Optional[int] = None,
    132         width: Optional[int] = None,
    133         height: Optional[int] = None,
    134     ):
    135         """
    136         Move the window or resize it without having to change all properties.
    137         """
    138 
    139         rect = self.area.rect
    140         if xpos is not None:
    141             rect.x = xpos
    142 
    143         if ypos is not None:
    144             rect.y = ypos
    145 
    146         if width is not None:
    147             rect.width = width
    148 
    149         if height is not None:
    150             rect.height = height
    151 
    152         self.area.rect = rect
    153 
    154     def select_text(
    155         self, start_anchor, end_anchor=None, include_trailing_whitespace=False
    156     ):
    157         """
    158         Selects the word corresponding to start_anchor. If end_anchor supplied, selects
    159         from start_anchor to the end of end_anchor. If include_trailing_whitespace=True
    160         then also selects trailing space characters (useful for delete).
    161         """
    162 
    163         start_index, end_index, last_space_index = self.anchor_to_range(start_anchor)
    164         if end_anchor is not None:
    165             _, end_index, last_space_index = self.anchor_to_range(end_anchor)
    166 
    167         if include_trailing_whitespace:
    168             end_index = last_space_index
    169 
    170         self.area.sel = Span(start_index, end_index)
    171 
    172     def position_caret(self, anchor, after=False):
    173         """
    174         Positions the caret before the given anchor. If after=True position it directly after.
    175         """
    176 
    177         start_index, end_index, _ = self.anchor_to_range(anchor)
    178         index = end_index if after else start_index
    179 
    180         self.area.sel = index
    181 
    182     def anchor_to_range(self, anchor):
    183         anchors_data = calculate_text_anchors(self._get_visible_text(), self.area.sel.left)
    184         for loop_anchor, start_index, end_index, last_space_index in anchors_data:
    185             if anchor == loop_anchor:
    186                 return (start_index, end_index, last_space_index)
    187 
    188         raise RuntimeError(f"Couldn't find anchor {anchor}")
    189 
    190     def _update_labels(self, _visible_text):
    191         """
    192         Updates the position of the labels displayed on top of each word
    193         """
    194 
    195         anchors_data = calculate_text_anchors(self._get_visible_text(), self.area.sel.left)
    196         return [
    197             (Span(start_index, end_index), anchor)
    198             for anchor, start_index, end_index, _ in anchors_data
    199         ]
    200 
    201     def _get_visible_text(self):
    202         # Placeholder for a future method of getting this
    203         return self.area.value
    204 
    205 
    206 if False:
    207     # Some code for testing, change above False to True and edit as desired
    208     draft_manager = DraftManager()
    209     draft_manager.show(
    210         "This is a line of text\nand another line of text and some more text so that the line gets so long that it wraps a bit.\nAnd a final sentence"
    211     )
    212     draft_manager.reposition(xpos=100, ypos=100)
    213     draft_manager.select_text("c")