import os import re import time import talon from .create_spoken_forms import create_spoken_forms from talon import Context, Module, app, imgui, ui, fs, actions from glob import glob from itertools import islice from pathlib import Path # Construct at startup a list of overides for application names (similar to how homophone list is managed) # ie for a given talon recognition word set `one note`, recognized this in these switcher functions as `ONENOTE` # the list is a comma seperated `, ` # TODO: Consider put list csv's (homophones.csv, app_name_overrides.csv) files together in a seperate directory,`knausj_talon/lists` cwd = os.path.dirname(os.path.realpath(__file__)) overrides_directory = os.path.join(cwd, "app_names") override_file_name = f"app_name_overrides.{talon.app.platform}.csv" override_file_path = os.path.join(overrides_directory, override_file_name) mod = Module() mod.list("running", desc="all running applications") mod.list("launch", desc="all launchable applications") ctx = Context() # a list of the current overrides overrides = {} # a list of the currently running application names running_application_dict = {} mac_application_directories = [ "/Applications", "/Applications/Utilities", "/System/Applications", "/System/Applications/Utilities", ] # windows_application_directories = [ # "%AppData%/Microsoft/Windows/Start Menu/Programs", # "%ProgramData%/Microsoft/Windows/Start Menu/Programs", # "%AppData%/Microsoft/Internet Explorer/Quick Launch/User Pinned/TaskBar", # ] words_to_exclude = [ "and", "zero", "one", "two", "three", "for", "four", "five", "six", "seven", "eight", "nine", "microsoft", "windows", "Windows", ] # windows-specific logic if app.platform == "windows": import os import ctypes import pywintypes import pythoncom import winerror try: import winreg except ImportError: # Python 2 import _winreg as winreg bytes = lambda x: str(buffer(x)) from ctypes import wintypes from win32com.shell import shell, shellcon from win32com.propsys import propsys, pscon # KNOWNFOLDERID # https://msdn.microsoft.com/en-us/library/dd378457 # win32com defines most of these, except the ones added in Windows 8. FOLDERID_AppsFolder = pywintypes.IID("{1e87508d-89c2-42f0-8a7e-645a0f50ca58}") # win32com is missing SHGetKnownFolderIDList, so use ctypes. _ole32 = ctypes.OleDLL("ole32") _shell32 = ctypes.OleDLL("shell32") _REFKNOWNFOLDERID = ctypes.c_char_p _PPITEMIDLIST = ctypes.POINTER(ctypes.c_void_p) _ole32.CoTaskMemFree.restype = None _ole32.CoTaskMemFree.argtypes = (wintypes.LPVOID,) _shell32.SHGetKnownFolderIDList.argtypes = ( _REFKNOWNFOLDERID, # rfid wintypes.DWORD, # dwFlags wintypes.HANDLE, # hToken _PPITEMIDLIST, ) # ppidl def get_known_folder_id_list(folder_id, htoken=None): if isinstance(folder_id, pywintypes.IIDType): folder_id = bytes(folder_id) pidl = ctypes.c_void_p() try: _shell32.SHGetKnownFolderIDList(folder_id, 0, htoken, ctypes.byref(pidl)) return shell.AddressAsPIDL(pidl.value) except WindowsError as e: if e.winerror & 0x80070000 == 0x80070000: # It's a WinAPI error, so re-raise it, letting Python # raise a specific exception such as FileNotFoundError. raise ctypes.WinError(e.winerror & 0x0000FFFF) raise finally: if pidl: _ole32.CoTaskMemFree(pidl) def enum_known_folder(folder_id, htoken=None): id_list = get_known_folder_id_list(folder_id, htoken) folder_shell_item = shell.SHCreateShellItem(None, None, id_list) items_enum = folder_shell_item.BindToHandler( None, shell.BHID_EnumItems, shell.IID_IEnumShellItems ) result = [] for item in items_enum: # print(item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY)) result.append(item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY)) return result def list_known_folder(folder_id, htoken=None): result = [] for item in enum_known_folder(folder_id, htoken): result.append(item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY)) result.sort(key=lambda x: x.upper()) return result @mod.capture(rule="{self.running}") # | )") def running_applications(m) -> str: "Returns a single application name" try: return m.running except AttributeError: return m.text @mod.capture(rule="{self.launch}") def launch_applications(m) -> str: "Returns a single application name" return m.launch def update_lists(): global running_application_dict running_application_dict = {} running = {} for cur_app in ui.apps(background=False): name = cur_app.name spoken_forms = create_spoken_forms(name, words_to_exclude=words_to_exclude) for spoken_form in spoken_forms: if spoken_form not in running: running[spoken_form] = cur_app.name running_application_dict[cur_app.name] = True print(running) for override in overrides: running[override] = overrides[override] lists = { "self.running": running, # "self.launch": launch, } # batch update lists ctx.lists.update(lists) def update_overrides(name, flags): """Updates the overrides list""" global overrides overrides = {} if name is None or name == override_file_path: # print("update_overrides") with open(override_file_path, "r") as f: for line in f: line = line.rstrip() line = line.split(",") if len(line) == 2: overrides[line[0].lower()] = line[1].strip() update_lists() @mod.action_class class Actions: def get_running_app(name: str) -> ui.App: """Get the first available running app with `name`.""" # We should use the capture result directly if it's already in the list # of running applications. Otherwise, name is from and we # can be a bit fuzzier if name not in running_application_dict: if len(name) < 3: raise RuntimeError( f'Skipped getting app: "{name}" has less than 3 chars.' ) for running_name, full_application_name in ctx.lists[ "self.running" ].items(): if running_name == name or running_name.lower().startswith( name.lower() ): name = full_application_name break for app in ui.apps(): if app.name == name and not app.background: return app raise RuntimeError(f'App not running: "{name}"') def switcher_focus(name: str): """Focus a new application by name""" app = actions.user.get_running_app(name) app.focus() # Hacky solution to do this reliably on Mac. timeout = 5 t1 = time.monotonic() if talon.app.platform == "mac": while ui.active_app() != app and time.monotonic() - t1 < timeout: time.sleep(0.1) def switcher_launch(path: str): """Launch a new application by path""" if app.platform == "windows": is_valid_path = False try: current_path = Path(path) is_valid_path = current_path.is_file() # print("valid path: {}".format(is_valid_path)) except: # print("invalid path") is_valid_path = False if is_valid_path: # print("path: " + path) ui.launch(path=path) else: # print("envelop") actions.key("super-s") actions.sleep("300ms") actions.insert("apps: {}".format(path)) actions.sleep("150ms") actions.key("enter") else: ui.launch(path=path) def switcher_toggle_running(): """Shows/hides all running applications""" if gui.showing: gui.hide() else: gui.show() def switcher_hide_running(): """Hides list of running applications""" gui.hide() @imgui.open() def gui(gui: imgui.GUI): gui.text("Names of running applications") gui.line() for line in ctx.lists["self.running"]: gui.text(line) def update_launch_list(): launch = {} if app.platform == "mac": for base in mac_application_directories: if os.path.isdir(base): for name in os.listdir(base): path = os.path.join(base, name) name = name.rsplit(".", 1)[0].lower() launch[name] = path words = name.split(" ") for word in words: if word and word not in launch: if len(name) > 6 and len(word) < 3: continue launch[word] = path elif app.platform == "windows": shortcuts = enum_known_folder(FOLDERID_AppsFolder) # str(shortcuts) for name in shortcuts: # print("hit: " + name) # print(name) # name = path.rsplit("\\")[-1].split(".")[0].lower() if "install" not in name: spoken_form = create_spoken_forms(name) # print(spoken_form) launch[spoken_form] = name words = spoken_form.split(" ") for word in words: if word not in words_to_exclude and word not in launch: if len(name) > 6 and len(word) < 3: continue launch[word] = name ctx.lists["self.launch"] = launch def ui_event(event, arg): if event in ("app_launch", "app_close"): update_lists() # Currently update_launch_list only does anything on mac, so we should make sure # to initialize user launch to avoid getting "List not found: user.launch" # errors on other platforms. ctx.lists["user.launch"] = {} ctx.lists["user.running"] = {} # Talon starts faster if you don't use the `talon.ui` module during launch def on_ready(): update_overrides(None, None) fs.watch(overrides_directory, update_overrides) update_launch_list() ui.register("", ui_event) # NOTE: please update this from "launch" to "ready" in Talon v0.1.5 app.register("ready", on_ready)