dotfiles

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

draft_talon_helpers.py (9326B)


      1 from typing import Optional
      2 from talon import ui, settings, Module, Context, actions
      3 from .draft_ui import DraftManager
      4 
      5 mod = Module()
      6 
      7 # ctx is for toggling the draft_window_showing variable
      8 # which lets you execute actions whenever the window is visible.
      9 ctx = Context()
     10 
     11 # ctx_focused is active only when the draft window is focussed. This
     12 # lets you execute actions under that condition.
     13 ctx_focused = Context()
     14 ctx_focused.matches = r"""
     15 title: Talon Draft
     16 """
     17 
     18 mod.tag("draft_window_showing", desc="Tag set when draft window showing")
     19 setting_theme = mod.setting(
     20     "draft_window_theme",
     21     type=str,
     22     default="dark",
     23     desc="Sets the main colors of the window, one of 'dark' or 'light'",
     24 )
     25 setting_label_size = mod.setting(
     26     "draft_window_label_size",
     27     type=int,
     28     default=20,
     29     desc="Sets the size of the word labels used in the draft window",
     30 )
     31 setting_label_color = mod.setting(
     32     "draft_window_label_color",
     33     type=str,
     34     default=None,
     35     desc=(
     36         "Sets the color of the word labels used in the draft window. "
     37         "E.g. 00ff00 would be green"
     38     ),
     39 )
     40 setting_text_size = mod.setting(
     41     "draft_window_text_size",
     42     type=int,
     43     default=20,
     44     desc="Sets the size of the text used in the draft window",
     45 )
     46 
     47 
     48 draft_manager = DraftManager()
     49 
     50 # Update the styling of the draft window dynamically as user settings change
     51 def _update_draft_style(*args):
     52     draft_manager.set_styling(
     53         **{
     54             arg: setting.get()
     55             for setting, arg in (
     56                 (setting_theme, "theme"),
     57                 (setting_label_size, "label_size"),
     58                 (setting_label_color, "label_color"),
     59                 (setting_text_size, "text_size"),
     60             )
     61         }
     62     )
     63 
     64 
     65 settings.register("", _update_draft_style)
     66 
     67 
     68 @ctx_focused.action_class("user")
     69 class ContextSensitiveDictationActions:
     70     """
     71     Override these actions to assist 'Smart dictation mode'.
     72     see https://github.com/knausj85/knausj_talon/pull/356
     73     """
     74 
     75     def dictation_peek_left(clobber=False):
     76         area = draft_manager.area
     77         return area[max(0, area.sel.left - 50) : area.sel.left]
     78 
     79     def dictation_peek_right():
     80         area = draft_manager.area
     81         return area[area.sel.right : area.sel.right + 50]
     82 
     83     def paste(text: str):
     84         # todo: remove once user.paste works reliably with the draft window
     85         actions.insert(text)
     86 
     87 
     88 @ctx_focused.action_class("edit")
     89 class EditActions:
     90     """
     91     Make default edit actions more efficient.
     92     """
     93 
     94     def selected_text() -> str:
     95         area = draft_manager.area
     96         if area.sel:
     97             result = area[area.sel.left : area.sel.right]
     98             return result
     99         return ""
    100 
    101 
    102 from talon import cron
    103 
    104 
    105 class UndoWorkaround:
    106     """
    107     Workaround for the experimental textarea's undo being character by character.
    108     This keeps a debounced undo history. Can be deleted once this todo item is
    109     fixed: https://github.com/talonvoice/talon/issues/254#issuecomment-789149734
    110     """
    111 
    112     # Set this to False if you want to turn it off, or just delete all references
    113     # to this class
    114     enable_workaround = True
    115 
    116     # Stack of (text_value, selection) tuples representing the undo stack
    117     undo_stack = []
    118     # Stack of (text_value, selection) tuples representing the redo stack
    119     redo_stack = []
    120     # Used by the timer to check when the text has stopped changing
    121     pending_undo = None
    122 
    123     # timer handle
    124     timer_handle = None
    125 
    126     @classmethod
    127     def start_logger(cls, reset_undo_stack: bool):
    128         if reset_undo_stack:
    129             cls.undo_stack = []
    130             cls.redo_stack = []
    131 
    132         cls.stop_logger()
    133         cls.timer_handle = cron.interval("500ms", cls._log_changes)
    134 
    135     @classmethod
    136     def stop_logger(cls):
    137         if cls.timer_handle is not None:
    138             cron.cancel(cls.timer_handle)
    139         cls.timer_handle = None
    140         cls.pending_undo = None
    141 
    142     @classmethod
    143     def perform_undo(cls):
    144         if len(cls.undo_stack) == 0:
    145             return
    146 
    147         curr_text = draft_manager.area.value
    148         curr_sel = (draft_manager.area.sel.left, draft_manager.area.sel.right)
    149         text, sel = cls.undo_stack[-1]
    150         if text == curr_text:
    151             cls.undo_stack.pop()
    152             if len(cls.undo_stack) == 0:
    153                 return
    154 
    155             # Most of the time (unless user has only just finished updating) the
    156             # top of the stack will have the same contents as the text area. In
    157             # this case pop again to get a bit lower. We should never have the
    158             # same text twice, hence we don't need a loop.
    159             text, sel = cls.undo_stack[-1]
    160 
    161         # Remember the current state in the redo stack
    162         cls.redo_stack.append((curr_text, curr_sel))
    163         draft_manager.area.value = text
    164         draft_manager.area.sel = sel
    165 
    166         cls.pending_undo = (text, sel)
    167 
    168     @classmethod
    169     def perform_redo(cls):
    170         if len(cls.redo_stack) == 0:
    171             return
    172 
    173         text, sel = cls.redo_stack.pop()
    174 
    175         draft_manager.area.value = text
    176         draft_manager.area.sel = sel
    177 
    178         cls.pending_undo = (text, sel)
    179         cls.undo_stack.append((text, sel))
    180 
    181     @classmethod
    182     def _log_changes(cls):
    183         """
    184         If the text and cursor position hasn't changed for two interval iterations
    185         (1s) and the undo stack doesn't match the current state, then add to the stack.
    186         """
    187 
    188         curr_val = draft_manager.area.value
    189         # Turn the Span into a tuple, because we can't == Spans
    190         curr_sel = (draft_manager.area.sel.left, draft_manager.area.sel.right)
    191         curr_state = (curr_val, curr_sel)
    192 
    193         state_stack_mismatch = (
    194             len(cls.undo_stack) == 0
    195             or
    196             # Only want to update the undo stack if the value has changed, not just
    197             # the selection
    198             curr_state[0] != cls.undo_stack[-1][0]
    199         )
    200 
    201         if cls.pending_undo == curr_state and state_stack_mismatch:
    202             cls.undo_stack.append(curr_state)
    203             # Clear out the redo stack because we've changed the text
    204             cls.redo_stack = []
    205         elif cls.pending_undo != curr_state:
    206             cls.pending_undo = curr_state
    207         elif not state_stack_mismatch and len(cls.undo_stack) > 0:
    208             # Remember the cursor position in the undo stack for the current text value
    209             cls.undo_stack[-1] = (cls.undo_stack[-1][0], curr_sel)
    210         else:
    211             # The text area text is not changing, do nothing
    212             pass
    213 
    214 
    215 if UndoWorkaround.enable_workaround:
    216     ctx_focused.action("edit.undo")(UndoWorkaround.perform_undo)
    217     ctx_focused.action("edit.redo")(UndoWorkaround.perform_redo)
    218 
    219 
    220 @mod.action_class
    221 class Actions:
    222     def draft_show(text: Optional[str] = None):
    223         """
    224         Shows draft window
    225         """
    226 
    227         draft_manager.show(text)
    228         UndoWorkaround.start_logger(text is not None)
    229         ctx.tags = ["user.draft_window_showing"]
    230 
    231     def draft_hide():
    232         """
    233         Hides draft window
    234         """
    235 
    236         draft_manager.hide()
    237         UndoWorkaround.stop_logger()
    238         ctx.tags = []
    239 
    240     def draft_select(
    241         start_anchor: str, end_anchor: str = "", include_trailing_whitespace: int = 0
    242     ):
    243         """
    244         Selects text in the draft window
    245         """
    246 
    247         draft_manager.select_text(
    248             start_anchor,
    249             end_anchor=None if end_anchor == "" else end_anchor,
    250             include_trailing_whitespace=include_trailing_whitespace == 1,
    251         )
    252 
    253     def draft_position_caret(anchor: str, after: int = 0):
    254         """
    255         Positions the caret in the draft window
    256         """
    257 
    258         draft_manager.position_caret(anchor, after=after == 1)
    259 
    260     def draft_get_text() -> str:
    261         """
    262         Returns the text in the draft window
    263         """
    264 
    265         return draft_manager.get_text()
    266 
    267     def draft_resize(width: int, height: int):
    268         """
    269         Resize the draft window.
    270         """
    271 
    272         draft_manager.reposition(width=width, height=height)
    273 
    274     def draft_named_move(name: str, screen_number: Optional[int] = None):
    275         """
    276         Lets you move the window to the top, bottom, left, right, or middle
    277         of the screen.
    278         """
    279 
    280         screen = ui.screens()[screen_number or 0]
    281         window_rect = draft_manager.get_rect()
    282         xpos = (screen.width - window_rect.width) / 2
    283         ypos = (screen.height - window_rect.height) / 2
    284 
    285         if name == "top":
    286             ypos = 50
    287         elif name == "bottom":
    288             ypos = screen.height - window_rect.height - 50
    289         elif name == "left":
    290             xpos = 50
    291         elif name == "right":
    292             xpos = screen.width - window_rect.width - 50
    293         elif name == "middle":
    294             # That's the default values
    295             pass
    296 
    297         # Adjust for the fact that the screen may not be at 0,0.
    298         xpos += screen.x
    299         ypos += screen.y
    300         draft_manager.reposition(xpos=xpos, ypos=ypos)
    301 
    302 
    303 # Some capture groups we need
    304 
    305 
    306 @mod.capture(rule="{self.letter}+")
    307 def draft_anchor(m) -> str:
    308     """
    309     An anchor (string of letters)
    310     """
    311     return "".join(m)
    312 
    313 
    314 @mod.capture(rule="(top|bottom|left|right|middle)")
    315 def draft_window_position(m) -> str:
    316     """
    317     One of the named positions you can move the window to
    318     """
    319 
    320     return "".join(m)