commit ffc230cbbe0554c04c44745b2a5417f41df21e08
parent 4444fd66bb157936b12dbc399dc3d239556e8bb5
Author: Alex Balgavy <alex@balgavy.eu>
Date: Tue, 1 Jun 2021 14:51:49 +0200
talon: starting a configuration
Diffstat:
16 files changed, 1062 insertions(+), 0 deletions(-)
diff --git a/dot.map b/dot.map
@@ -56,6 +56,7 @@ alacritty: ~/.config/alacritty
glow: ~/.config/glow
dunst: ~/.config/dunst
sxhkd: ~/.config/sxhkd
+talon-user: ~/.talon/user
# Email: these won't exist when you clone the repo
mbsync: ~/.config/mbsync
diff --git a/talon-user/caffeinate.talon b/talon-user/caffeinate.talon
@@ -0,0 +1,2 @@
+caffeinate start: user.system_command('killall caffeinate; setsid -f caffeinate -d && notify "Caffeinated" "Sleep disabled" talon')
+caffeinate stop: user.system_command('killall caffeinate && notify "Sleepy" "Will sleep again" talon')
diff --git a/talon-user/desktops.py b/talon-user/desktops.py
@@ -0,0 +1,65 @@
+import contextlib
+import time
+
+from talon import actions, ctrl, Module, ui, Context
+
+
+mod = Module()
+
+
+@mod.action_class
+class ModuleActions:
+ def desktop(number: int):
+ "change the current desktop"
+
+ def window_move_desktop_left():
+ """move the current window to the desktop to the left"""
+
+ def window_move_desktop_right():
+ """move the current window to the desktop to the right"""
+
+ def window_move_desktop(desktop_number: int):
+ """move the current window to a different desktop"""
+
+
+ctx = Context()
+ctx.matches = r"""
+os: mac
+"""
+
+
+@contextlib.contextmanager
+def _drag_window_mac(win=None):
+ if win is None:
+ win = ui.active_window()
+ fs = win.children.find(AXSubrole="AXFullScreenButton")[0]
+ rect = fs.AXFrame["$rect2d"]
+ x = rect["x"] + rect["width"] + 5
+ y = rect["y"] + rect["height"] / 2
+ ctrl.mouse_move(x, y)
+ ctrl.mouse_click(button=0, down=True)
+ yield
+ time.sleep(0.1)
+ ctrl.mouse_click(button=0, up=True)
+
+
+@ctx.action_class("self")
+class MacActions:
+ def desktop(number: int):
+ if number < 10:
+ actions.key("ctrl-{}".format(number))
+
+ def window_move_desktop_left():
+ with _drag_window_mac():
+ actions.key("ctrl-cmd-alt-left")
+
+ def window_move_desktop_right():
+ with _drag_window_mac():
+ actions.key("ctrl-cmd-alt-right")
+
+ def window_move_desktop(desktop_number: int):
+ if ui.apps(bundle="com.amethyst.Amethyst"):
+ actions.key(f"ctrl-alt-shift-{desktop_number}")
+ else:
+ with _drag_window_mac():
+ actions.key(f"ctrl-{desktop_number}")
diff --git a/talon-user/desktops.talon b/talon-user/desktops.talon
@@ -0,0 +1,10 @@
+# TODO: Once implementations exist for other platforms, maybe remove this
+# restriction.
+os: mac
+-
+desk <number>: user.desktop(number)
+desk left: key(ctrl-left)
+desk right: key(ctrl-right)
+window move desk <number>: user.window_move_desktop(number)
+window move desk left: user.window_move_desktop_left()
+window move desk right: user.window_move_desktop_right()
diff --git a/talon-user/engines.py b/talon-user/engines.py
@@ -0,0 +1,4 @@
+from talon import speech_system
+from talon.engines.w2l import W2lEngine
+w2l = W2lEngine(model='en_US', debug=False)
+speech_system.add_engine(w2l)
diff --git a/talon-user/exec.py b/talon-user/exec.py
@@ -0,0 +1,17 @@
+import os
+import subprocess
+
+from talon import Module
+
+mod = Module()
+
+
+@mod.action_class
+class Actions:
+ def system_command(cmd: str):
+ """execute a command on the system"""
+ os.system('. ~/.dotfiles/shell/env; . ~/.dotfiles/shell/paths;' + cmd)
+
+ def system_command_nb(cmd: str):
+ """execute a command on the system without blocking"""
+ subprocess.Popen(cmd, shell=True)
diff --git a/talon-user/help.py b/talon-user/help.py
@@ -0,0 +1,625 @@
+from collections import defaultdict
+import itertools
+import math
+from typing import Dict, List, Iterable, Set, Tuple, Union
+
+from talon import Module, Context, actions, imgui, Module, registry, ui, app
+from talon.grammar import Phrase
+
+mod = Module()
+mod.list("help_contexts", desc="list of available contexts")
+mod.mode("help", "mode for commands that are available only when help is visible")
+setting_help_max_contexts_per_page = mod.setting(
+ "help_max_contexts_per_page",
+ type=int,
+ default=20,
+ desc="Max contexts to display per page in help",
+)
+setting_help_max_command_lines_per_page = mod.setting(
+ "help_max_command_lines_per_page",
+ type=int,
+ default=50,
+ desc="Max lines of command to display per page in help",
+)
+
+ctx = Context()
+# context name -> commands
+context_command_map = {}
+
+# rule word -> Set[(context name, rule)]
+rule_word_map: Dict[str, Set[Tuple[str, str]]] = defaultdict(set)
+search_phrase = None
+
+# context name -> actual context
+context_map = {}
+
+current_context_page = 1
+sorted_context_map_keys = []
+
+selected_context = None
+selected_context_page = 1
+
+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:
+ if selected_context == None:
+ refresh_context_command_map(show_enabled_contexts_only)
+ else:
+ update_active_contexts_cache(registry.active_contexts())
+
+
+# todo: dynamic rect?
+@imgui.open(y=0)
+def gui_alphabet(gui: imgui.GUI):
+ global alphabet
+ gui.text("Alphabet help")
+ gui.line()
+
+ for key, val in alphabet.items():
+ gui.text("{}: {}".format(val, key))
+
+ gui.spacer()
+ if gui.button("close"):
+ gui_alphabet.hide()
+
+
+def format_context_title(context_name: str) -> str:
+ global cached_active_contexts_list
+ return "{} [{}]".format(
+ context_name,
+ "ACTIVE"
+ if context_map.get(context_name, None) in cached_active_contexts_list
+ else "INACTIVE",
+ )
+
+
+def format_context_button(index: int, context_label: str, context_name: str) -> str:
+ global cached_active_contexts_list
+ global show_enabled_contexts_only
+
+ if not show_enabled_contexts_only:
+ return "{}. {}{}".format(
+ index,
+ context_label,
+ "*"
+ if context_map.get(context_name, None) in cached_active_contexts_list
+ else "",
+ )
+ else:
+ return "{}. {} ".format(index, context_label)
+
+
+# translates 1-based index -> actual index in sorted_context_map_keys
+def get_context_page(index: int) -> int:
+ return math.ceil(index / setting_help_max_contexts_per_page.get())
+
+
+def get_total_context_pages() -> int:
+ return math.ceil(
+ len(sorted_context_map_keys) / setting_help_max_contexts_per_page.get()
+ )
+
+
+def get_current_context_page_length() -> int:
+ start_index = (current_context_page - 1) * setting_help_max_contexts_per_page.get()
+ return len(
+ sorted_context_map_keys[
+ start_index : start_index + setting_help_max_contexts_per_page.get()
+ ]
+ )
+
+
+def get_command_line_count(command: Tuple[str, str]) -> int:
+ """This should be kept in sync with draw_commands
+ """
+ _, body = command
+ lines = len(body.split("\n"))
+ if lines == 1:
+ return 1
+ else:
+ return lines + 1
+
+
+def get_pages(item_line_counts: List[int]) -> List[int]:
+ """Given some set of indivisible items with given line counts,
+ return the page number each item should appear on.
+
+ If an item will cross a page boundary, it is moved to the next page,
+ so that pages may be shorter than the maximum lenth, but not longer. The only
+ exception is when an item is longer than the maximum page length, in which
+ case that item will be placed on a longer page.
+ """
+ current_page_line_count = 0
+ current_page = 1
+ pages = []
+ for line_count in item_line_counts:
+ if (
+ line_count + current_page_line_count
+ > setting_help_max_command_lines_per_page.get()
+ ):
+ if current_page_line_count == 0:
+ # Special case, render a larger page.
+ page = current_page
+ current_page_line_count = 0
+ else:
+ page = current_page + 1
+ current_page_line_count = line_count
+ current_page += 1
+ else:
+ current_page_line_count += line_count
+ page = current_page
+ pages.append(page)
+ return pages
+
+
+@imgui.open(y=0)
+def gui_context_help(gui: imgui.GUI):
+ global context_command_map
+ global current_context_page
+ global selected_context
+ global selected_context_page
+ global sorted_context_map_keys
+ global show_enabled_contexts_only
+ global cached_active_contexts_list
+ global total_page_count
+ global search_phrase
+
+ # if no selected context, draw the contexts
+ if selected_context is None and search_phrase is None:
+ total_page_count = get_total_context_pages()
+
+ if not show_enabled_contexts_only:
+ gui.text(
+ "Help: All ({}/{}) (* = active)".format(
+ current_context_page, total_page_count
+ )
+ )
+ else:
+ gui.text(
+ "Help: Active Contexts Only ({}/{})".format(
+ current_context_page, total_page_count
+ )
+ )
+
+ gui.line()
+
+ current_item_index = 1
+ current_selection_index = 1
+ for key in sorted_context_map_keys:
+ if key in ctx.lists["self.help_contexts"]:
+ target_page = get_context_page(current_item_index)
+
+ if current_context_page == target_page:
+ button_name = format_context_button(
+ current_selection_index,
+ key,
+ ctx.lists["self.help_contexts"][key],
+ )
+
+ if gui.button(button_name):
+ selected_context = ctx.lists["self.help_contexts"][key]
+ current_selection_index = current_selection_index + 1
+
+ current_item_index += 1
+
+ if total_page_count > 1:
+ gui.spacer()
+ if gui.button("Next..."):
+ actions.user.help_next()
+
+ if gui.button("Previous..."):
+ actions.user.help_previous()
+
+ # if there's a selected context, draw the commands for it
+ else:
+ if selected_context is not None:
+ draw_context_commands(gui)
+ elif search_phrase is not None:
+ draw_search_commands(gui)
+
+ gui.spacer()
+ if total_page_count > 1:
+ if gui.button("Next..."):
+ actions.user.help_next()
+
+ if gui.button("Previous..."):
+ actions.user.help_previous()
+
+ if gui.button("Return"):
+ actions.user.help_return()
+
+ if gui.button("Refresh"):
+ actions.user.help_refresh()
+
+ if gui.button("Close"):
+ actions.user.help_hide()
+
+
+def draw_context_commands(gui: imgui.GUI):
+ global selected_context
+ global total_page_count
+ global selected_context_page
+
+ context_title = format_context_title(selected_context)
+ title = f"Context: {context_title}"
+ commands = context_command_map[selected_context].items()
+ item_line_counts = [get_command_line_count(command) for command in commands]
+ pages = get_pages(item_line_counts)
+ total_page_count = max(pages, default=1)
+ draw_commands_title(gui, title)
+
+ filtered_commands = [
+ command
+ for command, page in zip(commands, pages)
+ if page == selected_context_page
+ ]
+
+ draw_commands(gui, filtered_commands)
+
+
+def draw_search_commands(gui: imgui.GUI):
+ global search_phrase
+ global total_page_count
+ global cached_active_contexts_list
+ global selected_context_page
+
+ title = f"Search: {search_phrase}"
+ commands_grouped = get_search_commands(search_phrase)
+ commands_flat = list(itertools.chain.from_iterable(commands_grouped.values()))
+
+ sorted_commands_grouped = sorted(
+ commands_grouped.items(),
+ key=lambda item: context_map[item[0]] not in cached_active_contexts_list,
+ )
+
+ pages = get_pages(
+ [
+ sum(get_command_line_count(command) for command in commands) + 3
+ for _, commands in sorted_commands_grouped
+ ]
+ )
+ total_page_count = max(pages, default=1)
+
+ draw_commands_title(gui, title)
+
+ current_item_index = 1
+ for (context, commands), page in zip(sorted_commands_grouped, pages):
+ if page == selected_context_page:
+ gui.text(format_context_title(context))
+ gui.line()
+ draw_commands(gui, commands)
+ gui.spacer()
+
+
+def get_search_commands(phrase: str) -> Dict[str, Tuple[str, str]]:
+ global rule_word_map
+ tokens = search_phrase.split(" ")
+
+ viable_commands = rule_word_map[tokens[0]]
+ for token in tokens[1:]:
+ viable_commands &= rule_word_map[token]
+
+ commands_grouped = defaultdict(list)
+ for context, rule in viable_commands:
+ command = context_command_map[context][rule]
+ commands_grouped[context].append((rule, command))
+
+ return commands_grouped
+
+
+def draw_commands_title(gui: imgui.GUI, title: str):
+ global selected_context_page
+ global total_page_count
+
+ gui.text("{} ({}/{})".format(title, selected_context_page, total_page_count))
+ gui.line()
+
+
+def draw_commands(gui: imgui.GUI, commands: Iterable[Tuple[str, str]]):
+ for key, val in commands:
+ val = val.split("\n")
+ if len(val) > 1:
+ gui.text("{}:".format(key))
+ for line in val:
+ gui.text(" {}".format(line))
+ else:
+ gui.text("{}: {}".format(key, val[0]))
+
+
+def reset():
+ global current_context_page
+ global sorted_context_map_keys
+ global selected_context
+ global search_phrase
+ global selected_context_page
+ global cached_window_title
+ global show_enabled_contexts_only
+
+ current_context_page = 1
+ sorted_context_map_keys = None
+ selected_context = None
+ search_phrase = None
+ selected_context_page = 1
+ cached_window_title = None
+ show_enabled_contexts_only = False
+
+
+def update_active_contexts_cache(active_contexts):
+ # print("update_active_contexts_cache")
+ global cached_active_contexts_list
+ cached_active_contexts_list = active_contexts
+
+
+# example usage todo: make a list definable in .talon
+# overrides = {"generic browser" : "broswer"}
+overrides = {}
+
+
+def refresh_context_command_map(enabled_only=False):
+ global rule_word_map
+ global context_command_map
+ global context_map
+ global sorted_context_map_keys
+ global show_enabled_contexts_only
+ global cached_window_title
+ global context_map
+
+ context_map = {}
+ cached_short_context_names = {}
+ 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(".")
+ index = -1
+ if "talon" in splits[index]:
+ index = -2
+ short_name = splits[index].replace("_", " ")
+ else:
+ short_name = splits[index].replace("_", " ")
+
+ if "mac" == short_name or "win" == short_name or "linux" == short_name:
+ index = index - 1
+ short_name = splits[index].replace("_", " ")
+
+ # print("short name: " + short_name)
+ if short_name in overrides:
+ short_name = overrides[short_name]
+
+ if enabled_only and context in active_contexts or not enabled_only:
+ context_command_map[context_name] = {}
+ for command_alias, val in context.commands.items():
+ # print(str(val))
+ if command_alias in registry.commands:
+ # print(str(val.rule.rule) + ": " + val.target.code)
+ 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)
+ else:
+ cached_short_context_names[short_name] = context_name
+ context_map[context_name] = context
+
+ refresh_rule_word_map(context_command_map)
+
+ ctx.lists["self.help_contexts"] = cached_short_context_names
+ # print(str(ctx.lists["self.help_contexts"]))
+ sorted_context_map_keys = sorted(cached_short_context_names)
+
+
+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():
+ for rule in commands:
+ tokens = set(token for token in rule.split(" ") if token.isalpha())
+ for token in tokens:
+ rule_word_map[token].add((context_name, rule))
+
+
+events_registered = False
+
+
+def register_events(register: bool):
+ global events_registered
+ if register:
+ if not events_registered and live_update:
+ events_registered = True
+ # registry.register('post:update_contexts', contexts_updated)
+ registry.register("update_commands", commands_updated)
+ else:
+ events_registered = False
+ # registry.unregister('post:update_contexts', contexts_updated)
+ registry.unregister("update_commands", commands_updated)
+
+
+@mod.action_class
+class Actions:
+ def help_alphabet(ab: dict):
+ """Provides the alphabet dictionary"""
+ # what you say is stored as a trigger
+ global alphabet
+ alphabet = ab
+ reset()
+ # print("help_alphabet - alphabet gui_alphabet: {}".format(gui_alphabet.showing))
+ # print(
+ # "help_alphabet - gui_context_help showing: {}".format(
+ # gui_context_help.showing
+ # )
+ # )
+ gui_context_help.hide()
+ gui_alphabet.hide()
+ gui_alphabet.show()
+ register_events(False)
+ actions.mode.enable("user.help")
+
+ def help_context_enabled():
+ """Display contextual command info"""
+ reset()
+ refresh_context_command_map(enabled_only=True)
+ gui_alphabet.hide()
+ gui_context_help.show()
+ register_events(True)
+ actions.mode.enable("user.help")
+
+ def help_context():
+ """Display contextual command info"""
+ reset()
+ refresh_context_command_map()
+ gui_alphabet.hide()
+ gui_context_help.show()
+ register_events(True)
+ actions.mode.enable("user.help")
+
+ def help_search(phrase: str):
+ """Display command info for search phrase"""
+ global search_phrase
+
+ reset()
+ search_phrase = phrase
+ refresh_context_command_map()
+ gui_alphabet.hide()
+ gui_context_help.show()
+ register_events(True)
+ actions.mode.enable("user.help")
+
+ def help_selected_context(m: str):
+ """Display command info for selected context"""
+ global selected_context
+ global selected_context_page
+
+ if not gui_context_help.showing:
+ reset()
+ refresh_context_command_map()
+ else:
+ selected_context_page = 1
+ update_active_contexts_cache(registry.active_contexts())
+
+ selected_context = m
+ gui_alphabet.hide()
+ gui_context_help.show()
+ register_events(True)
+ actions.mode.enable("user.help")
+
+ def help_next():
+ """Navigates to next page"""
+ global current_context_page
+ global selected_context
+ global selected_context_page
+ global total_page_count
+
+ if gui_context_help.showing:
+ if selected_context is None and search_phrase is None:
+ if current_context_page != total_page_count:
+ current_context_page += 1
+ else:
+ current_context_page = 1
+ else:
+ if selected_context_page != total_page_count:
+ selected_context_page += 1
+ else:
+ selected_context_page = 1
+
+ def help_select_index(index: int):
+ """Select the context by a number"""
+ global sorted_context_map_keys, selected_context
+ if gui_context_help.showing:
+ if index < setting_help_max_contexts_per_page.get() and (
+ (current_context_page - 1) * setting_help_max_contexts_per_page.get()
+ + index
+ < len(sorted_context_map_keys)
+ ):
+ if selected_context is None:
+ selected_context = ctx.lists["self.help_contexts"][
+ sorted_context_map_keys[
+ (current_context_page - 1)
+ * setting_help_max_contexts_per_page.get()
+ + index
+ ]
+ ]
+
+ def help_previous():
+ """Navigates to previous page"""
+ global current_context_page
+ global selected_context
+ global selected_context_page
+ global total_page_count
+
+ if gui_context_help.showing:
+ if selected_context is None and search_phrase is None:
+ if current_context_page != 1:
+ current_context_page -= 1
+ else:
+ current_context_page = total_page_count
+
+ else:
+ if selected_context_page != 1:
+ selected_context_page -= 1
+ else:
+ selected_context_page = total_page_count
+
+ def help_return():
+ """Returns to the main help window"""
+ global selected_context
+ global selected_context_page
+ global show_enabled_contexts_only
+
+ if gui_context_help.showing:
+ refresh_context_command_map(show_enabled_contexts_only)
+ selected_context_page = 1
+ selected_context = None
+
+ def help_refresh():
+ """Refreshes the help"""
+ global show_enabled_contexts_only
+ global selected_context
+
+ if gui_context_help.showing:
+ if selected_context == None:
+ refresh_context_command_map(show_enabled_contexts_only)
+ else:
+ update_active_contexts_cache(registry.active_contexts())
+
+ def help_hide():
+ """Hides the help"""
+ reset()
+
+ # print("help_hide - alphabet gui_alphabet: {}".format(gui_alphabet.showing))
+ # print(
+ # "help_hide - gui_context_help showing: {}".format(gui_context_help.showing)
+ # )
+
+ gui_alphabet.hide()
+ gui_context_help.hide()
+ refresh_context_command_map()
+ register_events(False)
+ actions.mode.disable("user.help")
+
+
+def commands_updated(_):
+ update_title()
+
+
+app.register("ready", refresh_context_command_map)
+
diff --git a/talon-user/help.talon b/talon-user/help.talon
@@ -0,0 +1,6 @@
+help alphabet: user.help_alphabet(user.get_alphabet())
+help context$: user.help_context()
+help active$: user.help_context_enabled()
+help search <user.text>$: user.help_search(text)
+help context {user.help_contexts}$: user.help_selected_context(help_contexts)
+help help: user.help_search("help")
diff --git a/talon-user/history.py b/talon-user/history.py
@@ -0,0 +1,78 @@
+from talon import imgui, Module, speech_system, actions, app
+
+# We keep command_history_size lines of history, but by default display only
+# command_history_display of them.
+mod = Module()
+setting_command_history_size = mod.setting("command_history_size", int, default=50)
+setting_command_history_display = mod.setting(
+ "command_history_display", int, default=10
+)
+
+hist_more = False
+history = []
+
+
+def parse_phrase(word_list):
+ return " ".join(word.split("\\")[0] for word in word_list)
+
+
+def on_phrase(j):
+ global history
+
+ try:
+ val = parse_phrase(getattr(j["parsed"], "_unmapped", j["phrase"]))
+ except:
+ val = parse_phrase(j["phrase"])
+
+ if val != "":
+ history.append(val)
+ history = history[-setting_command_history_size.get() :]
+
+
+# todo: dynamic rect?
+@imgui.open(y=0)
+def gui(gui: imgui.GUI):
+ global history
+ gui.text("Command History")
+ gui.line()
+ text = (
+ history[:] if hist_more else history[-setting_command_history_display.get() :]
+ )
+ for line in text:
+ gui.text(line)
+
+
+speech_system.register("phrase", on_phrase)
+
+
+@mod.action_class
+class Actions:
+ def history_toggle():
+ """Toggles viewing the history"""
+ if gui.showing:
+ gui.hide()
+ else:
+ gui.show()
+
+ def history_enable():
+ """Enables the history"""
+ gui.show()
+
+ def history_disable():
+ """Disables the history"""
+ gui.hide()
+
+ def history_clear():
+ """Clear the history"""
+ global history
+ history = []
+
+ def history_more():
+ """Show more history"""
+ global hist_more
+ hist_more = True
+
+ def history_less():
+ """Show less history"""
+ global hist_more
+ hist_more = False
diff --git a/talon-user/history.talon b/talon-user/history.talon
@@ -0,0 +1,4 @@
+command history: user.history_toggle()
+command history clear: user.history_clear()
+command history less: user.history_less()
+command history more: user.history_more()
diff --git a/talon-user/music.talon b/talon-user/music.talon
@@ -0,0 +1 @@
+now playing: user.system_command('notify "Current song" "$(mpc current)" talon')
diff --git a/talon-user/numbers.py b/talon-user/numbers.py
@@ -0,0 +1,186 @@
+from talon import Context, Module, actions
+from typing import List, Optional, Union, Iterator
+
+mod = Module()
+ctx = Context()
+
+digits = "zero one two three four five six seven eight nine".split()
+teens = "eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen".split()
+tens = "ten twenty thirty forty fifty sixty seventy eighty ninety".split()
+scales = "hundred thousand million billion trillion quadrillion quintillion sextillion septillion octillion nonillion decillion".split()
+
+digits_map = {n: i for i, n in enumerate(digits)}
+digits_map["oh"] = 0
+teens_map = {n: i + 11 for i, n in enumerate(teens)}
+tens_map = {n: 10 * (i + 1) for i, n in enumerate(tens)}
+scales_map = {n: 10 ** (3 * (i+1)) for i, n in enumerate(scales[1:])}
+scales_map["hundred"] = 100
+
+numbers_map = digits_map.copy()
+numbers_map.update(teens_map)
+numbers_map.update(tens_map)
+numbers_map.update(scales_map)
+
+def parse_number(l: List[str]) -> str:
+ """Parses a list of words into a number/digit string."""
+ l = list(scan_small_numbers(l))
+ for scale in scales:
+ l = parse_scale(scale, l)
+ return "".join(str(n) for n in l)
+
+def scan_small_numbers(l: List[str]) -> Iterator[Union[str,int]]:
+ """
+ Takes a list of number words, yields a generator of mixed numbers & strings.
+ Translates small number terms (<100) into corresponding numbers.
+ Drops all occurrences of "and".
+ Smashes digits onto tens words, eg. ["twenty", "one"] -> [21].
+ But note that "ten" and "zero" are excluded, ie:
+ ["ten", "three"] -> [10, 3]
+ ["fifty", "zero"] -> [50, 0]
+ Does nothing to scale words ("hundred", "thousand", "million", etc).
+ """
+ # reversed so that repeated pop() visits in left-to-right order
+ l = [x for x in reversed(l) if x != "and"]
+ while l:
+ n = l.pop()
+ # fuse tens onto digits, eg. "twenty", "one" -> 21
+ if n in tens_map and n != "ten" and l and digits_map.get(l[-1], 0) != 0:
+ d = l.pop()
+ yield numbers_map[n] + numbers_map[d]
+ # turn small number terms into corresponding numbers
+ elif n not in scales_map:
+ yield numbers_map[n]
+ else:
+ yield n
+
+def parse_scale(scale: str, l: List[Union[str,int]]) -> List[Union[str,int]]:
+ """Parses a list of mixed numbers & strings for occurrences of the following
+ pattern:
+
+ <multiplier> <scale> <remainder>
+
+ where <scale> is a scale word like "hundred", "thousand", "million", etc and
+ multiplier and remainder are numbers or strings of numbers of the
+ appropriate size. For example:
+
+ parse_scale("hundred", [1, "hundred", 2]) -> [102]
+ parse_scale("thousand", [12, "thousand", 3, 45]) -> [12345]
+
+ We assume that all scales of lower magnitude have already been parsed; don't
+ call parse_scale("thousand") until you've called parse_scale("hundred").
+ """
+ scale_value = scales_map[scale]
+ scale_digits = len(str(scale_value))
+
+ # Split the list on the desired scale word, then parse from left to right.
+ left, *splits = split_list(scale, l)
+ for right in splits:
+ # (1) Figure out the multiplier by looking to the left of the scale
+ # word. We ignore non-integers because they are scale words that we
+ # haven't processed yet; this strategy means that "thousand hundred"
+ # gets parsed as 1,100 instead of 100,000, but "hundred thousand" is
+ # parsed correctly as 100,000.
+ before = 1 # default multiplier
+ if left and isinstance(left[-1], int) and left[-1] != 0:
+ before = left.pop()
+
+ # (2) Absorb numbers to the right, eg. in [1, "thousand", 1, 26], "1
+ # thousand" absorbs ["1", "26"] to make 1,126. We pull numbers off
+ # `right` until we fill up the desired number of digits.
+ after = ""
+ while right and isinstance(right[0], int):
+ next = after + str(right[0])
+ if len(next) >= scale_digits: break
+ after = next
+ right.pop(0)
+ after = int(after) if after else 0
+
+ # (3) Push the parsed number into place, append whatever was left
+ # unparsed, and continue.
+ left.append(before * scale_value + after)
+ left.extend(right)
+
+ return left
+
+def split_list(value, l: list) -> Iterator:
+ """Splits a list by occurrences of a given value."""
+ start = 0
+ while True:
+ try: i = l.index(value, start)
+ except ValueError: break
+ yield l[start:i]
+ start = i+1
+ yield l[start:]
+
+
+# # ---------- TESTS (uncomment to run) ----------
+# def test_number(expected, string):
+# print('testing:', string)
+# l = list(scan_small_numbers(string.split()))
+# print(" scan --->", l)
+# for scale in scales:
+# old = l
+# l = parse_scale(scale, l)
+# if scale in old: print(" parse -->", l)
+# else: assert old == l, "parse_scale should do nothing if the scale does not occur in the list"
+# result = "".join(str(n) for n in l)
+# assert result == parse_number(string.split())
+# assert str(expected) == result, f"parsing {string!r}, expected {expected}, got {result}"
+
+# test_number(105000, "one hundred and five thousand")
+# test_number(1000000, "one thousand thousand")
+# test_number(1501000, "one million five hundred one thousand")
+# test_number(1501106, "one million five hundred and one thousand one hundred and six")
+# test_number(123, "one two three")
+# test_number(123, "one twenty three")
+# test_number(104, "ten four") # borderline, but valid in some dialects
+# test_number(1066, "ten sixty six") # a common way of saying years
+# test_number(1906, "nineteen oh six") # year
+# test_number(2001, "twenty oh one") # year
+# test_number(2020, "twenty twenty")
+# test_number(1001, "one thousand one")
+# test_number(1010, "one thousand ten")
+# test_number(123456, "one hundred and twenty three thousand and four hundred and fifty six")
+# test_number(123456, "one twenty three thousand four fifty six")
+
+# ## failing (and somewhat debatable) tests from old numbers.py
+# #test_number(10000011, "one million one one")
+# #test_number(100001010, "one million ten ten")
+# #test_number(1050006000, "one hundred thousand and five thousand and six thousand")
+
+
+# ---------- CAPTURES ----------
+alt_digits = "(" + ("|".join(digits_map.keys())) + ")"
+alt_teens = "(" + ("|".join(teens_map.keys())) + ")"
+alt_tens = "(" + ("|".join(tens_map.keys())) + ")"
+alt_scales = "(" + ("|".join(scales_map.keys())) + ")"
+number_word = "(" + "|".join(numbers_map.keys()) + ")"
+
+# TODO: allow things like "double eight" for 88
+@ctx.capture("digit_string", rule=f"({alt_digits} | {alt_teens} | {alt_tens})+")
+def digit_string(m) -> str: return parse_number(list(m))
+
+@ctx.capture("digits", rule="<digit_string>")
+def digits(m) -> int:
+ """Parses a phrase representing a digit sequence, returning it as an integer."""
+ return int(m.digit_string)
+
+@mod.capture(rule=f"{number_word}+ (and {number_word}+)*")
+def number_string(m) -> str:
+ """Parses a number phrase, returning that number as a string."""
+ return parse_number(list(m))
+
+@ctx.capture("number", rule="<user.number_string>")
+def number(m) -> int:
+ """Parses a number phrase, returning it as an integer."""
+ return int(m.number_string)
+
+@ctx.capture("number_signed", rule=f"[negative|minus] <number>")
+def number_signed(m):
+ number = m[-1]
+ return -number if (m[0] in ["negative", "minus"]) else number
+
+@ctx.capture(
+ "number_small", rule=f"({alt_digits} | {alt_teens} | {alt_tens} [{alt_digits}])"
+)
+def number_small(m): return int(parse_number(list(m)))
diff --git a/talon-user/settings.talon b/talon-user/settings.talon
@@ -0,0 +1,17 @@
+-
+settings():
+ #adjust the scale of the imgui to my liking
+ imgui.scale = 1.3
+
+ #set the max number of command lines per page in help
+ user.help_max_command_lines_per_page = 50
+
+ # set the max number of contexts display per page in help
+ user.help_max_contexts_per_page = 20
+
+ # the number of lines of command history to display by default
+ user.command_history_display = 10
+
+ # the number of lines of command history to keep in total;
+ # "command history more" to display all of them, "command history less" to restore
+ user.command_history_size = 50
diff --git a/talon-user/settings/additional_words.csv b/talon-user/settings/additional_words.csv
@@ -0,0 +1,10 @@
+Word(s),Spoken Form (If Different)
+nmap,N map
+under-documented,under documented
+nmap
+admin
+Cisco
+Citrix
+VPN
+DNS
+Minecraft
diff --git a/talon-user/settings/search_engines.csv b/talon-user/settings/search_engines.csv
@@ -0,0 +1,6 @@
+URL Template,Name
+https://www.amazon.com/s/?field-keywords=%s,amazon
+https://www.google.com/search?q=%s,google
+https://maps.google.com/maps?q=%s,map
+https://scholar.google.com/scholar?q=%s,scholar
+https://en.wikipedia.org/w/index.php?search=%s,wiki
diff --git a/talon-user/settings/words_to_replace.csv b/talon-user/settings/words_to_replace.csv
@@ -0,0 +1,30 @@
+Replacement,Original
+I,i
+I'm,i'm
+I've,i've
+I'll,i'll
+I'd,i'd
+Monday,monday
+Mondays,mondays
+Tuesday,tuesday
+Tuesdays,tuesdays
+Wednesday,wednesday
+Wednesdays,wednesdays
+Thursday,thursday
+Thursdays,thursdays
+Friday,friday
+Fridays,fridays
+Saturday,saturday
+Saturdays,saturdays
+Sunday,sunday
+Sundays,sundays
+January,january
+February,february
+April,april
+June,june
+July,july
+August,august
+September,september
+October,october
+November,november
+December,december