dotfiles

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

help.py (19457B)


      1 from collections import defaultdict
      2 import itertools
      3 import math
      4 from typing import Dict, List, Iterable, Set, Tuple, Union
      5 
      6 from talon import Module, Context, actions, imgui, Module, registry, ui, app
      7 from talon.grammar import Phrase
      8 
      9 mod = Module()
     10 mod.list("help_contexts", desc="list of available contexts")
     11 mod.mode("help", "mode for commands that are available only when help is visible")
     12 setting_help_max_contexts_per_page = mod.setting(
     13     "help_max_contexts_per_page",
     14     type=int,
     15     default=20,
     16     desc="Max contexts to display per page in help",
     17 )
     18 setting_help_max_command_lines_per_page = mod.setting(
     19     "help_max_command_lines_per_page",
     20     type=int,
     21     default=50,
     22     desc="Max lines of command to display per page in help",
     23 )
     24 
     25 ctx = Context()
     26 # context name -> commands
     27 context_command_map = {}
     28 
     29 # rule word -> Set[(context name, rule)]
     30 rule_word_map: Dict[str, Set[Tuple[str, str]]] = defaultdict(set)
     31 search_phrase = None
     32 
     33 # context name -> actual context
     34 context_map = {}
     35 
     36 current_context_page = 1
     37 sorted_context_map_keys = []
     38 
     39 selected_context = None
     40 selected_context_page = 1
     41 
     42 total_page_count = 1
     43 
     44 cached_active_contexts_list = []
     45 
     46 live_update = True
     47 cached_window_title = None
     48 show_enabled_contexts_only = False
     49 
     50 
     51 def update_title():
     52     global live_update
     53     global show_enabled_contexts_only
     54     global cached_window_title
     55 
     56     if live_update:
     57         if gui_context_help.showing:
     58             if selected_context == None:
     59                 refresh_context_command_map(show_enabled_contexts_only)
     60             else:
     61                 update_active_contexts_cache(registry.active_contexts())
     62 
     63 
     64 # todo: dynamic rect?
     65 @imgui.open(y=0)
     66 def gui_alphabet(gui: imgui.GUI):
     67     global alphabet
     68     gui.text("Alphabet help")
     69     gui.line()
     70 
     71     for key, val in alphabet.items():
     72         gui.text("{}: {}".format(val, key))
     73 
     74     gui.spacer()
     75     if gui.button("close"):
     76         gui_alphabet.hide()
     77 
     78 
     79 def format_context_title(context_name: str) -> str:
     80     global cached_active_contexts_list
     81     return "{} [{}]".format(
     82         context_name,
     83         "ACTIVE"
     84         if context_map.get(context_name, None) in cached_active_contexts_list
     85         else "INACTIVE",
     86     )
     87 
     88 
     89 def format_context_button(index: int, context_label: str, context_name: str) -> str:
     90     global cached_active_contexts_list
     91     global show_enabled_contexts_only
     92 
     93     if not show_enabled_contexts_only:
     94         return "{}. {}{}".format(
     95             index,
     96             context_label,
     97             "*"
     98             if context_map.get(context_name, None) in cached_active_contexts_list
     99             else "",
    100         )
    101     else:
    102         return "{}. {} ".format(index, context_label)
    103 
    104 
    105 # translates 1-based index -> actual index in sorted_context_map_keys
    106 def get_context_page(index: int) -> int:
    107     return math.ceil(index / setting_help_max_contexts_per_page.get())
    108 
    109 
    110 def get_total_context_pages() -> int:
    111     return math.ceil(
    112         len(sorted_context_map_keys) / setting_help_max_contexts_per_page.get()
    113     )
    114 
    115 
    116 def get_current_context_page_length() -> int:
    117     start_index = (current_context_page - 1) * setting_help_max_contexts_per_page.get()
    118     return len(
    119         sorted_context_map_keys[
    120             start_index : start_index + setting_help_max_contexts_per_page.get()
    121         ]
    122     )
    123 
    124 
    125 def get_command_line_count(command: Tuple[str, str]) -> int:
    126     """This should be kept in sync with draw_commands
    127     """
    128     _, body = command
    129     lines = len(body.split("\n"))
    130     if lines == 1:
    131         return 1
    132     else:
    133         return lines + 1
    134 
    135 
    136 def get_pages(item_line_counts: List[int]) -> List[int]:
    137     """Given some set of indivisible items with given line counts,
    138     return the page number each item should appear on.
    139 
    140     If an item will cross a page boundary, it is moved to the next page,
    141     so that pages may be shorter than the maximum lenth, but not longer. The only
    142     exception is when an item is longer than the maximum page length, in which
    143     case that item will be placed on a longer page.
    144     """
    145     current_page_line_count = 0
    146     current_page = 1
    147     pages = []
    148     for line_count in item_line_counts:
    149         if (
    150             line_count + current_page_line_count
    151             > setting_help_max_command_lines_per_page.get()
    152         ):
    153             if current_page_line_count == 0:
    154                 # Special case, render a larger page.
    155                 page = current_page
    156                 current_page_line_count = 0
    157             else:
    158                 page = current_page + 1
    159                 current_page_line_count = line_count
    160             current_page += 1
    161         else:
    162             current_page_line_count += line_count
    163             page = current_page
    164         pages.append(page)
    165     return pages
    166 
    167 
    168 @imgui.open(y=0)
    169 def gui_context_help(gui: imgui.GUI):
    170     global context_command_map
    171     global current_context_page
    172     global selected_context
    173     global selected_context_page
    174     global sorted_context_map_keys
    175     global show_enabled_contexts_only
    176     global cached_active_contexts_list
    177     global total_page_count
    178     global search_phrase
    179 
    180     # if no selected context, draw the contexts
    181     if selected_context is None and search_phrase is None:
    182         total_page_count = get_total_context_pages()
    183 
    184         if not show_enabled_contexts_only:
    185             gui.text(
    186                 "Help: All ({}/{}) (* = active)".format(
    187                     current_context_page, total_page_count
    188                 )
    189             )
    190         else:
    191             gui.text(
    192                 "Help: Active Contexts Only ({}/{})".format(
    193                     current_context_page, total_page_count
    194                 )
    195             )
    196 
    197         gui.line()
    198 
    199         current_item_index = 1
    200         current_selection_index = 1
    201         for key in sorted_context_map_keys:
    202             if key in ctx.lists["self.help_contexts"]:
    203                 target_page = get_context_page(current_item_index)
    204 
    205                 if current_context_page == target_page:
    206                     button_name = format_context_button(
    207                         current_selection_index,
    208                         key,
    209                         ctx.lists["self.help_contexts"][key],
    210                     )
    211 
    212                     if gui.button(button_name):
    213                         selected_context = ctx.lists["self.help_contexts"][key]
    214                     current_selection_index = current_selection_index + 1
    215 
    216                 current_item_index += 1
    217 
    218         if total_page_count > 1:
    219             gui.spacer()
    220             if gui.button("Next..."):
    221                 actions.user.help_next()
    222 
    223             if gui.button("Previous..."):
    224                 actions.user.help_previous()
    225 
    226     # if there's a selected context, draw the commands for it
    227     else:
    228         if selected_context is not None:
    229             draw_context_commands(gui)
    230         elif search_phrase is not None:
    231             draw_search_commands(gui)
    232 
    233         gui.spacer()
    234         if total_page_count > 1:
    235             if gui.button("Next..."):
    236                 actions.user.help_next()
    237 
    238             if gui.button("Previous..."):
    239                 actions.user.help_previous()
    240 
    241         if gui.button("Return"):
    242             actions.user.help_return()
    243 
    244     if gui.button("Refresh"):
    245         actions.user.help_refresh()
    246 
    247     if gui.button("Close"):
    248         actions.user.help_hide()
    249 
    250 
    251 def draw_context_commands(gui: imgui.GUI):
    252     global selected_context
    253     global total_page_count
    254     global selected_context_page
    255 
    256     context_title = format_context_title(selected_context)
    257     title = f"Context: {context_title}"
    258     commands = context_command_map[selected_context].items()
    259     item_line_counts = [get_command_line_count(command) for command in commands]
    260     pages = get_pages(item_line_counts)
    261     total_page_count = max(pages, default=1)
    262     draw_commands_title(gui, title)
    263 
    264     filtered_commands = [
    265         command
    266         for command, page in zip(commands, pages)
    267         if page == selected_context_page
    268     ]
    269 
    270     draw_commands(gui, filtered_commands)
    271 
    272 
    273 def draw_search_commands(gui: imgui.GUI):
    274     global search_phrase
    275     global total_page_count
    276     global cached_active_contexts_list
    277     global selected_context_page
    278 
    279     title = f"Search: {search_phrase}"
    280     commands_grouped = get_search_commands(search_phrase)
    281     commands_flat = list(itertools.chain.from_iterable(commands_grouped.values()))
    282 
    283     sorted_commands_grouped = sorted(
    284         commands_grouped.items(),
    285         key=lambda item: context_map[item[0]] not in cached_active_contexts_list,
    286     )
    287 
    288     pages = get_pages(
    289         [
    290             sum(get_command_line_count(command) for command in commands) + 3
    291             for _, commands in sorted_commands_grouped
    292         ]
    293     )
    294     total_page_count = max(pages, default=1)
    295 
    296     draw_commands_title(gui, title)
    297 
    298     current_item_index = 1
    299     for (context, commands), page in zip(sorted_commands_grouped, pages):
    300         if page == selected_context_page:
    301             gui.text(format_context_title(context))
    302             gui.line()
    303             draw_commands(gui, commands)
    304             gui.spacer()
    305 
    306 
    307 def get_search_commands(phrase: str) -> Dict[str, Tuple[str, str]]:
    308     global rule_word_map
    309     tokens = search_phrase.split(" ")
    310 
    311     viable_commands = rule_word_map[tokens[0]]
    312     for token in tokens[1:]:
    313         viable_commands &= rule_word_map[token]
    314 
    315     commands_grouped = defaultdict(list)
    316     for context, rule in viable_commands:
    317         command = context_command_map[context][rule]
    318         commands_grouped[context].append((rule, command))
    319 
    320     return commands_grouped
    321 
    322 
    323 def draw_commands_title(gui: imgui.GUI, title: str):
    324     global selected_context_page
    325     global total_page_count
    326 
    327     gui.text("{} ({}/{})".format(title, selected_context_page, total_page_count))
    328     gui.line()
    329 
    330 
    331 def draw_commands(gui: imgui.GUI, commands: Iterable[Tuple[str, str]]):
    332     for key, val in commands:
    333         val = val.split("\n")
    334         if len(val) > 1:
    335             gui.text("{}:".format(key))
    336             for line in val:
    337                 gui.text("    {}".format(line))
    338         else:
    339             gui.text("{}: {}".format(key, val[0]))
    340 
    341 
    342 def reset():
    343     global current_context_page
    344     global sorted_context_map_keys
    345     global selected_context
    346     global search_phrase
    347     global selected_context_page
    348     global cached_window_title
    349     global show_enabled_contexts_only
    350 
    351     current_context_page = 1
    352     sorted_context_map_keys = None
    353     selected_context = None
    354     search_phrase = None
    355     selected_context_page = 1
    356     cached_window_title = None
    357     show_enabled_contexts_only = False
    358 
    359 
    360 def update_active_contexts_cache(active_contexts):
    361     # print("update_active_contexts_cache")
    362     global cached_active_contexts_list
    363     cached_active_contexts_list = active_contexts
    364 
    365 
    366 # example usage todo: make a list definable in .talon
    367 # overrides = {"generic browser" : "broswer"}
    368 overrides = {}
    369 
    370 
    371 def refresh_context_command_map(enabled_only=False):
    372     global rule_word_map
    373     global context_command_map
    374     global context_map
    375     global sorted_context_map_keys
    376     global show_enabled_contexts_only
    377     global cached_window_title
    378     global context_map
    379 
    380     context_map = {}
    381     cached_short_context_names = {}
    382     show_enabled_contexts_only = enabled_only
    383     cached_window_title = ui.active_window().title
    384     active_contexts = registry.active_contexts()
    385     # print(str(active_contexts))
    386     update_active_contexts_cache(active_contexts)
    387 
    388     context_command_map = {}
    389     for context_name, context in registry.contexts.items():
    390         splits = context_name.split(".")
    391         index = -1
    392         if "talon" in splits[index]:
    393             index = -2
    394             short_name = splits[index].replace("_", " ")
    395         else:
    396             short_name = splits[index].replace("_", " ")
    397 
    398         if "mac" == short_name or "win" == short_name or "linux" == short_name:
    399             index = index - 1
    400             short_name = splits[index].replace("_", " ")
    401 
    402         # print("short name: " + short_name)
    403         if short_name in overrides:
    404             short_name = overrides[short_name]
    405 
    406         if enabled_only and context in active_contexts or not enabled_only:
    407             context_command_map[context_name] = {}
    408             for command_alias, val in context.commands.items():
    409                 # print(str(val))
    410                 if command_alias in registry.commands:
    411                     # print(str(val.rule.rule) + ": " + val.target.code)
    412                     context_command_map[context_name][
    413                         str(val.rule.rule)
    414                     ] = val.target.code
    415             # print(short_name)
    416             # print("length: " + str(len(context_command_map[context_name])))
    417             if len(context_command_map[context_name]) == 0:
    418                 context_command_map.pop(context_name)
    419             else:
    420                 cached_short_context_names[short_name] = context_name
    421                 context_map[context_name] = context
    422 
    423     refresh_rule_word_map(context_command_map)
    424 
    425     ctx.lists["self.help_contexts"] = cached_short_context_names
    426     # print(str(ctx.lists["self.help_contexts"]))
    427     sorted_context_map_keys = sorted(cached_short_context_names)
    428 
    429 
    430 def refresh_rule_word_map(context_command_map):
    431     global rule_word_map
    432     rule_word_map = defaultdict(set)
    433 
    434     for context_name, commands in context_command_map.items():
    435         for rule in commands:
    436             tokens = set(token for token in rule.split(" ") if token.isalpha())
    437             for token in tokens:
    438                 rule_word_map[token].add((context_name, rule))
    439 
    440 
    441 events_registered = False
    442 
    443 
    444 def register_events(register: bool):
    445     global events_registered
    446     if register:
    447         if not events_registered and live_update:
    448             events_registered = True
    449             # registry.register('post:update_contexts', contexts_updated)
    450             registry.register("update_commands", commands_updated)
    451     else:
    452         events_registered = False
    453         # registry.unregister('post:update_contexts', contexts_updated)
    454         registry.unregister("update_commands", commands_updated)
    455 
    456 
    457 @mod.action_class
    458 class Actions:
    459     def help_alphabet(ab: dict):
    460         """Provides the alphabet dictionary"""
    461         # what you say is stored as a trigger
    462         global alphabet
    463         alphabet = ab
    464         reset()
    465         # print("help_alphabet - alphabet gui_alphabet: {}".format(gui_alphabet.showing))
    466         # print(
    467         #     "help_alphabet - gui_context_help showing: {}".format(
    468         #         gui_context_help.showing
    469         #     )
    470         # )
    471         gui_context_help.hide()
    472         gui_alphabet.hide()
    473         gui_alphabet.show()
    474         register_events(False)
    475         actions.mode.enable("user.help")
    476 
    477     def help_context_enabled():
    478         """Display contextual command info"""
    479         reset()
    480         refresh_context_command_map(enabled_only=True)
    481         gui_alphabet.hide()
    482         gui_context_help.show()
    483         register_events(True)
    484         actions.mode.enable("user.help")
    485 
    486     def help_context():
    487         """Display contextual command info"""
    488         reset()
    489         refresh_context_command_map()
    490         gui_alphabet.hide()
    491         gui_context_help.show()
    492         register_events(True)
    493         actions.mode.enable("user.help")
    494 
    495     def help_search(phrase: str):
    496         """Display command info for search phrase"""
    497         global search_phrase
    498 
    499         reset()
    500         search_phrase = phrase
    501         refresh_context_command_map()
    502         gui_alphabet.hide()
    503         gui_context_help.show()
    504         register_events(True)
    505         actions.mode.enable("user.help")
    506 
    507     def help_selected_context(m: str):
    508         """Display command info for selected context"""
    509         global selected_context
    510         global selected_context_page
    511 
    512         if not gui_context_help.showing:
    513             reset()
    514             refresh_context_command_map()
    515         else:
    516             selected_context_page = 1
    517             update_active_contexts_cache(registry.active_contexts())
    518 
    519         selected_context = m
    520         gui_alphabet.hide()
    521         gui_context_help.show()
    522         register_events(True)
    523         actions.mode.enable("user.help")
    524 
    525     def help_next():
    526         """Navigates to next page"""
    527         global current_context_page
    528         global selected_context
    529         global selected_context_page
    530         global total_page_count
    531 
    532         if gui_context_help.showing:
    533             if selected_context is None and search_phrase is None:
    534                 if current_context_page != total_page_count:
    535                     current_context_page += 1
    536                 else:
    537                     current_context_page = 1
    538             else:
    539                 if selected_context_page != total_page_count:
    540                     selected_context_page += 1
    541                 else:
    542                     selected_context_page = 1
    543 
    544     def help_select_index(index: int):
    545         """Select the context by a number"""
    546         global sorted_context_map_keys, selected_context
    547         if gui_context_help.showing:
    548             if index < setting_help_max_contexts_per_page.get() and (
    549                 (current_context_page - 1) * setting_help_max_contexts_per_page.get()
    550                 + index
    551                 < len(sorted_context_map_keys)
    552             ):
    553                 if selected_context is None:
    554                     selected_context = ctx.lists["self.help_contexts"][
    555                         sorted_context_map_keys[
    556                             (current_context_page - 1)
    557                             * setting_help_max_contexts_per_page.get()
    558                             + index
    559                         ]
    560                     ]
    561 
    562     def help_previous():
    563         """Navigates to previous page"""
    564         global current_context_page
    565         global selected_context
    566         global selected_context_page
    567         global total_page_count
    568 
    569         if gui_context_help.showing:
    570             if selected_context is None and search_phrase is None:
    571                 if current_context_page != 1:
    572                     current_context_page -= 1
    573                 else:
    574                     current_context_page = total_page_count
    575 
    576             else:
    577                 if selected_context_page != 1:
    578                     selected_context_page -= 1
    579                 else:
    580                     selected_context_page = total_page_count
    581 
    582     def help_return():
    583         """Returns to the main help window"""
    584         global selected_context
    585         global selected_context_page
    586         global show_enabled_contexts_only
    587 
    588         if gui_context_help.showing:
    589             refresh_context_command_map(show_enabled_contexts_only)
    590             selected_context_page = 1
    591             selected_context = None
    592 
    593     def help_refresh():
    594         """Refreshes the help"""
    595         global show_enabled_contexts_only
    596         global selected_context
    597 
    598         if gui_context_help.showing:
    599             if selected_context == None:
    600                 refresh_context_command_map(show_enabled_contexts_only)
    601             else:
    602                 update_active_contexts_cache(registry.active_contexts())
    603 
    604     def help_hide():
    605         """Hides the help"""
    606         reset()
    607 
    608         # print("help_hide - alphabet gui_alphabet: {}".format(gui_alphabet.showing))
    609         # print(
    610         #     "help_hide - gui_context_help showing: {}".format(gui_context_help.showing)
    611         # )
    612 
    613         gui_alphabet.hide()
    614         gui_context_help.hide()
    615         refresh_context_command_map()
    616         register_events(False)
    617         actions.mode.disable("user.help")
    618 
    619 
    620 def commands_updated(_):
    621     update_title()
    622 
    623 
    624 app.register("ready", refresh_context_command_map)
    625