dotfiles

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

commit ffc230cbbe0554c04c44745b2a5417f41df21e08
parent 4444fd66bb157936b12dbc399dc3d239556e8bb5
Author: Alex Balgavy <alex@balgavy.eu>
Date:   Tue,  1 Jun 2021 14:51:49 +0200

talon: starting a configuration

Diffstat:
Mdot.map | 1+
Atalon-user/caffeinate.talon | 2++
Atalon-user/desktops.py | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/desktops.talon | 10++++++++++
Atalon-user/engines.py | 4++++
Atalon-user/exec.py | 17+++++++++++++++++
Atalon-user/help.py | 625+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/help.talon | 6++++++
Atalon-user/history.py | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/history.talon | 4++++
Atalon-user/music.talon | 1+
Atalon-user/numbers.py | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/settings.talon | 17+++++++++++++++++
Atalon-user/settings/additional_words.csv | 10++++++++++
Atalon-user/settings/search_engines.csv | 6++++++
Atalon-user/settings/words_to_replace.csv | 30++++++++++++++++++++++++++++++
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