Repository: talonhub/community
Issue: Intermittent 'cron interval error' followed by KeyError in 'help search'

What happened here? The same voice command fails oddly, and then succeeds.

After seeing the problem, I was able to execute the command repeatedly with no problems. I tried restarting talon and, again, the command ran without problems.

Note that none of the files under my user dir were being edited or changed at the time.

```
337602 2021-05-20 10:32:03    IO EMIT ['help', 'search', 'win']
337603 2021-05-20 10:32:03    IO DECODED ['help', 'search', 'win']
337604 2021-05-20 10:32:03 ERROR cron interval error <bound method GUI._tick of <talon.imgui.GUI object at 0x000000002A4AFBB0>>
337605    12:                   threading.py:912* # cron thread
337606    11:                   threading.py:954*
337607    10:                   threading.py:892*
337608     9:                  talon\\cron.py:129|
337609     8:                  talon\\cron.py:86 |
337610     7:        talon\\scripting\\rctx.py:239| # 'cron' user.knausj_talon.code.help:call()
337611     6:                  talon\\cron.py:152| # [stack splice]
337612     5:                 talon\\imgui.py:885|
337613     4:                 talon\\imgui.py:911|
337614     3: user\\knausj_talon\\code\\help.py:231| draw_search_commands(gui)
337615     2: user\\knausj_talon\\code\\help.py:283| sorted_commands_grouped = sorted(
337616     1: user\\knausj_talon\\code\\help.py:285| key=lambda item: context_map[item[0]] not in cached_active_contexts_list,
337617 KeyError: 'user.personal.apps.win.app.talon'
337618 2021-05-20 10:32:03    IO [audio]=1440.000ms [compile]=0.000ms [emit]=141.000ms (0.10x) [decode]=78.000ms (0.05x) [total]=219.000ms (0.       15x)
337619 2021-05-20 10:32:03    IO
337620 2021-05-20 10:32:25    IO EMIT ['help', 'surge', 'win']
337621 2021-05-20 10:32:25    IO COMPILING
337622 2021-05-20 10:32:25    IO grammar -> dfa -> cfg in 0.219000s
337623 2021-05-20 10:32:25    IO DECODED ['help', 'search', 'win']

```

For the record, here's the contents of the file in question:

```
okimo@kaiju:/mnt/c/Users/me/AppData/Roaming/talon/user$ cat personal/apps/win/app.talon
os: windows
-

action(user.window_restore):
    # note: use alt- option here because super- alternative doesn't really distinguish between restore and minimize.
        #key(super-down)
        key(alt-space r)

action(user.window_move):
        key(alt-space m)

action(user.window_size):
        key(alt-space s)

# duplicate of 'window hide' - how to avoid repeated code?
action(user.window_minimize):
    # note: use alt- option here because super- alternative doesn't really distinguish between restore and minimize.
        #key(super-down)
        key(alt-space n)

action(user.window_maximize):
        #key(super-up)
        key(alt-space x)

window restore | win rest: user.window_restore()
window move | win move: user.window_move()
window size | win size: user.window_size()
window minimize | win min: user.window_minimize()
window maximize | win max: user.window_maximize()

desky left : key(ctrl-super-left)
desky right : key(ctrl-super-right)

app switcher | app switch : key(ctrl-alt-tab)
win back: key(alt-tab)
task view | task show | taskee view : key(super-tab)
task bar | taskobar : key(super-t)
show menu | shomanoo : key(menu)

okimo@kaiju:/mnt/c/Users/me/AppData/Roaming/talon/user$

```

Changed files:
code/help.py

diff --git a/code/help.py b/code/help.py
index 4d8beab..34797b2 100644
--- a/code/help.py
+++ b/code/help.py
@@ -46,14 +46,12 @@ total_page_count = 1
 cached_active_contexts_list = []
 
 live_update = True
-cached_window_title = None
 show_enabled_contexts_only = False
 
 
 def update_title():
     global live_update
     global show_enabled_contexts_only
-    global cached_window_title
 
     if live_update:
         if gui_context_help.showing:
@@ -343,16 +341,14 @@ def reset():
     global selected_context
     global search_phrase
     global selected_context_page
-    global cached_window_title
     global show_enabled_contexts_only
     global display_name_to_context_name_map
 
     current_context_page = 1
-    sorted_display_list = None
+    sorted_display_list = []
     selected_context = None
     search_phrase = None
     selected_context_page = 1
-    cached_window_title = None
     show_enabled_contexts_only = False
     display_name_to_context_name_map = {}
 
@@ -369,25 +365,13 @@ overrides = {}
 
 
 def refresh_context_command_map(enabled_only=False):
-    global rule_word_map
-    global context_command_map
-    global context_map
-    global sorted_display_list
-    global show_enabled_contexts_only
-    global cached_window_title
-    global context_map
-    global display_name_to_context_name_map
+    active_contexts = registry.active_contexts()
 
-    context_map = {}
+    local_context_map = {}
+    local_display_name_to_context_name_map = {}
+    local_context_command_map = {}
     cached_short_context_names = {}
-    display_name_to_context_name_map = {}
-    show_enabled_contexts_only = enabled_only
-    cached_window_title = ui.active_window().title
-    active_contexts = registry.active_contexts()
-    # print(str(active_contexts))
-    update_active_contexts_cache(active_contexts)
 
-    context_command_map = {}
     for context_name, context in registry.contexts.items():
         splits = context_name.split(".")
 
@@ -404,35 +388,44 @@ def refresh_context_command_map(enabled_only=False):
                 short_names = [overrides[short_names[1]]]
 
             if enabled_only and context in active_contexts or not enabled_only:
-                context_command_map[context_name] = {}
+                local_context_command_map[context_name] = {}
                 for command_alias, val in context.commands.items():
-                    # print(str(val))
                     if command_alias in registry.commands or not enabled_only:
-                        # print(str(val.rule.rule) + ": " + val.target.code)
-                        context_command_map[context_name][
+                        local_context_command_map[context_name][
                             str(val.rule.rule)
                         ] = val.target.code
-                # print(short_name)
-                # print("length: " + str(len(context_command_map[context_name])))
-                if len(context_command_map[context_name]) == 0:
-                    context_command_map.pop(context_name)
+                if len(local_context_command_map[context_name]) == 0:
+                    local_context_command_map.pop(context_name)
                 else:
                     for short_name in short_names:
                         cached_short_context_names[short_name] = context_name
 
                     # the last entry will contain no symbols
-                    display_name_to_context_name_map[display_name] = context_name
-                    context_map[context_name] = context
+                    local_display_name_to_context_name_map[display_name] = context_name
+                    local_context_map[context_name] = context
+
 
-    refresh_rule_word_map(context_command_map)
+
+    # Update all the global state after we've performed our calculations
+    global context_map
+    global context_command_map
+    global sorted_display_list
+    global show_enabled_contexts_only
+    global display_name_to_context_name_map
+    global rule_word_map
+
+    context_map = local_context_map
+    context_command_map = local_context_command_map
+    sorted_display_list = sorted(local_display_name_to_context_name_map.keys())
+    show_enabled_contexts_only = enabled_only
+    display_name_to_context_name_map = local_display_name_to_context_name_map
+    rule_word_map = refresh_rule_word_map(local_context_command_map)
 
     ctx.lists["self.help_contexts"] = cached_short_context_names
-    # print(str(ctx.lists["self.help_contexts"]))
-    sorted_display_list = sorted(display_name_to_context_name_map.keys())
+    update_active_contexts_cache(active_contexts)
 
 
 def refresh_rule_word_map(context_command_map):
-    global rule_word_map
     rule_word_map = defaultdict(set)
 
     for context_name, commands in context_command_map.items():
@@ -441,6 +434,8 @@ def refresh_rule_word_map(context_command_map):
             for token in tokens:
                 rule_word_map[token].add((context_name, rule))
 
+    return rule_word_map
+
 
 events_registered = False
 
@@ -623,6 +618,3 @@ class Actions:
 
 def commands_updated(_):
     update_title()
-
-
-app.register("ready", refresh_context_command_map)