dotfiles/talon/community/community-cursorless-0.4.0/code/switcher.py
2024-11-16 20:27:38 -07:00

346 lines
11 KiB
Python

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 `<Recognized Words>, <Overide>`
# 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}") # | <user.text>)")
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 <user.text> 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)