dotfiles

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

formatters.py (9888B)


      1 from talon import Module, Context, actions, ui, imgui, app
      2 from talon.grammar import Phrase
      3 from typing import List, Union
      4 import logging
      5 import re
      6 
      7 ctx = Context()
      8 key = actions.key
      9 edit = actions.edit
     10 
     11 words_to_keep_lowercase = "a,an,the,at,by,for,in,is,of,on,to,up,and,as,but,or,nor".split(
     12     ","
     13 )
     14 
     15 # The last phrase spoken, without & with formatting. Used for reformatting.
     16 last_phrase = ""
     17 last_phrase_formatted = ""
     18 
     19 
     20 def surround(by):
     21     def func(i, word, last):
     22         if i == 0:
     23             word = by + word
     24         if last:
     25             word += by
     26         return word
     27 
     28     return func
     29 
     30 
     31 def format_phrase(m: Union[str, Phrase], fmtrs: str):
     32     global last_phrase, last_phrase_formatted
     33     last_phrase = m
     34     words = []
     35     if isinstance(m, str):
     36         words = m.split(" ")
     37     else:
     38         # TODO: is this still necessary, and if so why?
     39         if m.words[-1] == "over":
     40             m.words = m.words[:-1]
     41 
     42         words = actions.dictate.parse_words(m)
     43         words = actions.dictate.replace_words(words)
     44 
     45     result = last_phrase_formatted = format_phrase_no_history(words, fmtrs)
     46     actions.user.add_phrase_to_history(result)
     47     # Arguably, we shouldn't be dealing with history here, but somewhere later
     48     # down the line. But we have a bunch of code that relies on doing it this
     49     # way and I don't feel like rewriting it just now. -rntz, 2020-11-04
     50     return result
     51 
     52 
     53 def format_phrase_no_history(word_list, fmtrs: str):
     54     fmtr_list = fmtrs.split(",")
     55     words = []
     56     spaces = True
     57     for i, w in enumerate(word_list):
     58         for name in reversed(fmtr_list):
     59             smash, func = all_formatters[name]
     60             w = func(i, w, i == len(word_list) - 1)
     61             spaces = spaces and not smash
     62         words.append(w)
     63     sep = " " if spaces else ""
     64     return sep.join(words)
     65 
     66 
     67 NOSEP = True
     68 SEP = False
     69 
     70 
     71 def words_with_joiner(joiner):
     72     """Pass through words unchanged, but add a separator between them."""
     73 
     74     def formatter_function(i, word, _):
     75         return word if i == 0 else joiner + word
     76 
     77     return (NOSEP, formatter_function)
     78 
     79 
     80 def first_vs_rest(first_func, rest_func=lambda w: w):
     81     """Supply one or two transformer functions for the first and rest of
     82     words respectively.
     83 
     84     Leave second argument out if you want all but the first word to be passed
     85     through unchanged.
     86     Set first argument to None if you want the first word to be passed
     87     through unchanged."""
     88     if first_func is None:
     89         first_func = lambda w: w
     90 
     91     def formatter_function(i, word, _):
     92         return first_func(word) if i == 0 else rest_func(word)
     93 
     94     return formatter_function
     95 
     96 
     97 def every_word(word_func):
     98     """Apply one function to every word."""
     99 
    100     def formatter_function(i, word, _):
    101         return word_func(word)
    102 
    103     return formatter_function
    104 
    105 
    106 formatters_dict = {
    107     "NOOP": (SEP, lambda i, word, _: word),
    108     "DOUBLE_UNDERSCORE": (NOSEP, first_vs_rest(lambda w: "__%s__" % w)),
    109     "PRIVATE_CAMEL_CASE": (NOSEP, first_vs_rest(lambda w: w, lambda w: w.capitalize())),
    110     "PROTECTED_CAMEL_CASE": (
    111         NOSEP,
    112         first_vs_rest(lambda w: w, lambda w: w.capitalize()),
    113     ),
    114     "PUBLIC_CAMEL_CASE": (NOSEP, every_word(lambda w: w.capitalize())),
    115     "SNAKE_CASE": (
    116         NOSEP,
    117         first_vs_rest(lambda w: w.lower(), lambda w: "_" + w.lower()),
    118     ),
    119     "NO_SPACES": (NOSEP, every_word(lambda w: w)),
    120     "DASH_SEPARATED": words_with_joiner("-"),
    121     "TERMINAL_DASH_SEPARATED": (
    122         NOSEP,
    123         first_vs_rest(lambda w: " --" + w.lower(), lambda w: "-" + w.lower()),
    124     ),
    125     "DOUBLE_COLON_SEPARATED": words_with_joiner("::"),
    126     "ALL_CAPS": (SEP, every_word(lambda w: w.upper())),
    127     "ALL_LOWERCASE": (SEP, every_word(lambda w: w.lower())),
    128     "DOUBLE_QUOTED_STRING": (SEP, surround('"')),
    129     "SINGLE_QUOTED_STRING": (SEP, surround("'")),
    130     "SPACE_SURROUNDED_STRING": (SEP, surround(" ")),
    131     "DOT_SEPARATED": words_with_joiner("."),
    132     "DOT_SNAKE": (NOSEP, lambda i, word, _: "." + word if i == 0 else "_" + word),
    133     "SLASH_SEPARATED": (NOSEP, every_word(lambda w: "/" + w)),
    134     "CAPITALIZE_FIRST_WORD": (SEP, first_vs_rest(lambda w: w.capitalize())),
    135     "CAPITALIZE_ALL_WORDS": (
    136         SEP,
    137         lambda i, word, _: word.capitalize()
    138         if i == 0 or word not in words_to_keep_lowercase
    139         else word,
    140     ),
    141     "FIRST_THREE": (NOSEP, lambda i, word, _: word[0:3]),
    142     "FIRST_FOUR": (NOSEP, lambda i, word, _: word[0:4]),
    143     "FIRST_FIVE": (NOSEP, lambda i, word, _: word[0:5]),
    144 }
    145 
    146 # This is the mapping from spoken phrases to formatters
    147 formatters_words = {
    148     "allcaps": formatters_dict["ALL_CAPS"],
    149     "alldown": formatters_dict["ALL_LOWERCASE"],
    150     "camel": formatters_dict["PRIVATE_CAMEL_CASE"],
    151     "dotted": formatters_dict["DOT_SEPARATED"],
    152     "dubstring": formatters_dict["DOUBLE_QUOTED_STRING"],
    153     "dunder": formatters_dict["DOUBLE_UNDERSCORE"],
    154     "hammer": formatters_dict["PUBLIC_CAMEL_CASE"],
    155     "kebab": formatters_dict["DASH_SEPARATED"],
    156     "packed": formatters_dict["DOUBLE_COLON_SEPARATED"],
    157     "padded": formatters_dict["SPACE_SURROUNDED_STRING"],
    158     # "say": formatters_dict["NOOP"],
    159     # "sentence": formatters_dict["CAPITALIZE_FIRST_WORD"],
    160     "slasher": formatters_dict["SLASH_SEPARATED"],
    161     "smash": formatters_dict["NO_SPACES"],
    162     "snake": formatters_dict["SNAKE_CASE"],
    163     # "speak": formatters_dict["NOOP"],
    164     "string": formatters_dict["SINGLE_QUOTED_STRING"],
    165     "title": formatters_dict["CAPITALIZE_ALL_WORDS"],
    166     # disable a few formatters for now
    167     # "tree": formatters_dict["FIRST_THREE"],
    168     # "quad": formatters_dict["FIRST_FOUR"],
    169     # "fiver": formatters_dict["FIRST_FIVE"],
    170 }
    171 
    172 all_formatters = {}
    173 all_formatters.update(formatters_dict)
    174 all_formatters.update(formatters_words)
    175 
    176 mod = Module()
    177 mod.list("formatters", desc="list of formatters")
    178 mod.list(
    179     "prose_formatter",
    180     desc="words to start dictating prose, and the formatter they apply",
    181 )
    182 
    183 
    184 @mod.capture(rule="{self.formatters}+")
    185 def formatters(m) -> str:
    186     "Returns a comma-separated string of formatters e.g. 'SNAKE,DUBSTRING'"
    187     return ",".join(m.formatters_list)
    188 
    189 
    190 @mod.capture(
    191     # Note that if the user speaks something like "snake dot", it will
    192     # insert "dot" - otherwise, they wouldn't be able to insert punctuation
    193     # words directly.
    194     rule="<self.formatters> <user.text> (<user.text> | <user.formatter_immune>)*"
    195 )
    196 def format_text(m) -> str:
    197     "Formats the text and returns a string"
    198     out = ""
    199     formatters = m[0]
    200     for chunk in m[1:]:
    201         if isinstance(chunk, ImmuneString):
    202             out += chunk.string
    203         else:
    204             out += format_phrase(chunk, formatters)
    205     return out
    206 
    207 
    208 class ImmuneString(object):
    209     """Wrapper that makes a string immune from formatting."""
    210 
    211     def __init__(self, string):
    212         self.string = string
    213 
    214 
    215 @mod.capture(
    216     # Add anything else into this that you want to be able to speak during a
    217     # formatter.
    218     rule="(<user.symbol_key> | numb <number>)"
    219 )
    220 def formatter_immune(m) -> ImmuneString:
    221     """Text that can be interspersed into a formatter, e.g. characters.
    222 
    223     It will be inserted directly, without being formatted.
    224 
    225     """
    226     if hasattr(m, "number"):
    227         value = m.number
    228     else:
    229         value = m[0]
    230     return ImmuneString(str(value))
    231 
    232 
    233 @mod.action_class
    234 class Actions:
    235     def formatted_text(phrase: Union[str, Phrase], formatters: str) -> str:
    236         """Formats a phrase according to formatters. formatters is a comma-separated string of formatters (e.g. 'CAPITALIZE_ALL_WORDS,DOUBLE_QUOTED_STRING')"""
    237         return format_phrase(phrase, formatters)
    238 
    239     def insert_formatted(phrase: Union[str, Phrase], formatters: str):
    240         """Inserts a phrase formatted according to formatters. Formatters is a comma separated list of formatters (e.g. 'CAPITALIZE_ALL_WORDS,DOUBLE_QUOTED_STRING')"""
    241         actions.insert(format_phrase(phrase, formatters))
    242 
    243     def formatters_help_toggle():
    244         """Lists all formatters"""
    245         if gui.showing:
    246             gui.hide()
    247         else:
    248             gui.show()
    249 
    250     def formatters_reformat_last(formatters: str) -> str:
    251         """Clears and reformats last formatted phrase"""
    252         global last_phrase, last_phrase_formatted
    253         if actions.user.get_last_phrase() != last_phrase_formatted:
    254             # The last thing we inserted isn't the same as the last thing we
    255             # formatted, so abort.
    256             logging.warning(
    257                 "formatters_reformat_last(): Last phrase wasn't a formatter!"
    258             )
    259             return
    260         actions.user.clear_last_phrase()
    261         actions.user.insert_formatted(last_phrase, formatters)
    262 
    263     def formatters_reformat_selection(formatters: str) -> str:
    264         """Reformats the current selection."""
    265         selected = edit.selected_text()
    266         if not selected:
    267             print("Asked to reformat selection, but nothing selected!")
    268             return
    269         unformatted = re.sub(r"[^a-zA-Z0-9]+", " ", selected).lower()
    270         # TODO: Separate out camelcase & studleycase vars
    271 
    272         # Delete separately for compatibility with programs that don't overwrite
    273         # selected text (e.g. Emacs)
    274         edit.delete()
    275         text = actions.self.formatted_text(unformatted, formatters)
    276         actions.insert(text)
    277         return text
    278 
    279     def insert_many(strings: List[str]) -> None:
    280         """Insert a list of strings, sequentially."""
    281         for string in strings:
    282             actions.insert(string)
    283 
    284 
    285 ctx.lists["self.formatters"] = formatters_words.keys()
    286 ctx.lists["self.prose_formatter"] = {
    287     "say": "NOOP",
    288     "speak": "NOOP",
    289     "sentence": "CAPITALIZE_FIRST_WORD",
    290 }
    291 
    292 
    293 @imgui.open()
    294 def gui(gui: imgui.GUI):
    295     gui.text("List formatters")
    296     gui.line()
    297     for name in sorted(set(formatters_words.keys())):
    298         gui.text(f"{name} | {format_phrase_no_history(['one', 'two', 'three'], name)}")