dotfiles

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

switcher.py (11701B)


      1 import os
      2 import re
      3 import time
      4 
      5 import talon
      6 from talon import Context, Module, app, imgui, ui, fs, actions
      7 from glob import glob
      8 from itertools import islice
      9 from pathlib import Path
     10 
     11 # Construct at startup a list of overides for application names (similar to how homophone list is managed)
     12 # ie for a given talon recognition word set  `one note`, recognized this in these switcher functions as `ONENOTE`
     13 # the list is a comma seperated `<Recognized Words>, <Overide>`
     14 # TODO: Consider put list csv's (homophones.csv, app_name_overrides.csv) files together in a seperate directory,`knausj_talon/lists`
     15 cwd = os.path.dirname(os.path.realpath(__file__))
     16 overrides_directory = os.path.join(cwd, "app_names")
     17 override_file_name = f"app_name_overrides.{talon.app.platform}.csv"
     18 override_file_path = os.path.join(overrides_directory, override_file_name)
     19 
     20 
     21 mod = Module()
     22 mod.list("running", desc="all running applications")
     23 mod.list("launch", desc="all launchable applications")
     24 ctx = Context()
     25 
     26 # a list of the current overrides
     27 overrides = {}
     28 
     29 # a list of the currently running application names
     30 running_application_dict = {}
     31 
     32 
     33 mac_application_directories = [
     34     "/Applications",
     35     "/Applications/Utilities",
     36     "/System/Applications",
     37     "/System/Applications/Utilities",
     38 ]
     39 
     40 # windows_application_directories = [
     41 #     "%AppData%/Microsoft/Windows/Start Menu/Programs",
     42 #     "%ProgramData%/Microsoft/Windows/Start Menu/Programs",
     43 #     "%AppData%/Microsoft/Internet Explorer/Quick Launch/User Pinned/TaskBar",
     44 # ]
     45 
     46 words_to_exclude = [
     47     "and",
     48     "zero",
     49     "one",
     50     "two",
     51     "three",
     52     "for",
     53     "four",
     54     "five",
     55     "six",
     56     "seven",
     57     "eight",
     58     "nine",
     59     "microsoft",
     60     "windows",
     61     "Windows",
     62 ]
     63 
     64 # windows-specific logic
     65 if app.platform == "windows":
     66     import os
     67     import ctypes
     68     import pywintypes
     69     import pythoncom
     70     import winerror
     71 
     72     try:
     73         import winreg
     74     except ImportError:
     75         # Python 2
     76         import _winreg as winreg
     77 
     78         bytes = lambda x: str(buffer(x))
     79 
     80     from ctypes import wintypes
     81     from win32com.shell import shell, shellcon
     82     from win32com.propsys import propsys, pscon
     83 
     84     # KNOWNFOLDERID
     85     # https://msdn.microsoft.com/en-us/library/dd378457
     86     # win32com defines most of these, except the ones added in Windows 8.
     87     FOLDERID_AppsFolder = pywintypes.IID("{1e87508d-89c2-42f0-8a7e-645a0f50ca58}")
     88 
     89     # win32com is missing SHGetKnownFolderIDList, so use ctypes.
     90 
     91     _ole32 = ctypes.OleDLL("ole32")
     92     _shell32 = ctypes.OleDLL("shell32")
     93 
     94     _REFKNOWNFOLDERID = ctypes.c_char_p
     95     _PPITEMIDLIST = ctypes.POINTER(ctypes.c_void_p)
     96 
     97     _ole32.CoTaskMemFree.restype = None
     98     _ole32.CoTaskMemFree.argtypes = (wintypes.LPVOID,)
     99 
    100     _shell32.SHGetKnownFolderIDList.argtypes = (
    101         _REFKNOWNFOLDERID,  # rfid
    102         wintypes.DWORD,  # dwFlags
    103         wintypes.HANDLE,  # hToken
    104         _PPITEMIDLIST,
    105     )  # ppidl
    106 
    107     def get_known_folder_id_list(folder_id, htoken=None):
    108         if isinstance(folder_id, pywintypes.IIDType):
    109             folder_id = bytes(folder_id)
    110         pidl = ctypes.c_void_p()
    111         try:
    112             _shell32.SHGetKnownFolderIDList(folder_id, 0, htoken, ctypes.byref(pidl))
    113             return shell.AddressAsPIDL(pidl.value)
    114         except WindowsError as e:
    115             if e.winerror & 0x80070000 == 0x80070000:
    116                 # It's a WinAPI error, so re-raise it, letting Python
    117                 # raise a specific exception such as FileNotFoundError.
    118                 raise ctypes.WinError(e.winerror & 0x0000FFFF)
    119             raise
    120         finally:
    121             if pidl:
    122                 _ole32.CoTaskMemFree(pidl)
    123 
    124     def enum_known_folder(folder_id, htoken=None):
    125         id_list = get_known_folder_id_list(folder_id, htoken)
    126         folder_shell_item = shell.SHCreateShellItem(None, None, id_list)
    127         items_enum = folder_shell_item.BindToHandler(
    128             None, shell.BHID_EnumItems, shell.IID_IEnumShellItems
    129         )
    130         result = []
    131         for item in items_enum:
    132             # print(item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY))
    133             result.append(item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY))
    134 
    135         return result
    136 
    137     def list_known_folder(folder_id, htoken=None):
    138         result = []
    139         for item in enum_known_folder(folder_id, htoken):
    140             result.append(item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY))
    141         result.sort(key=lambda x: x.upper())
    142         return result
    143 
    144 
    145 @mod.capture(rule="{self.running}")  # | <user.text>)")
    146 def running_applications(m) -> str:
    147     "Returns a single application name"
    148     try:
    149         return m.running
    150     except AttributeError:
    151         return m.text
    152 
    153 
    154 @mod.capture(rule="{self.launch}")
    155 def launch_applications(m) -> str:
    156     "Returns a single application name"
    157     return m.launch
    158 
    159 
    160 def split_camel(word):
    161     return re.findall(r"[0-9A-Z]*[a-z]+(?=[A-Z]|$)", word)
    162 
    163 
    164 def get_words(name):
    165     words = re.findall(r"[0-9A-Za-z]+", name)
    166     out = []
    167     for word in words:
    168         out += split_camel(word)
    169     return out
    170 
    171 
    172 def update_lists():
    173     global running_application_dict
    174     running_application_dict = {}
    175     running = {}
    176     for cur_app in ui.apps(background=False):
    177         name = cur_app.name
    178 
    179         if name.endswith(".exe"):
    180             name = name.rsplit(".", 1)[0]
    181 
    182         words = get_words(name)
    183         for word in words:
    184             if word and word not in running and len(word) >= 3:
    185                 running[word.lower()] = cur_app.name
    186 
    187         running[name.lower()] = cur_app.name
    188         running_application_dict[cur_app.name] = True
    189 
    190     for override in overrides:
    191         running[override] = overrides[override]
    192 
    193     lists = {
    194         "self.running": running,
    195         # "self.launch": launch,
    196     }
    197 
    198     # batch update lists
    199     ctx.lists.update(lists)
    200 
    201 
    202 def update_overrides(name, flags):
    203     """Updates the overrides list"""
    204     global overrides
    205     overrides = {}
    206 
    207     if name is None or name == override_file_path:
    208         # print("update_overrides")
    209         with open(override_file_path, "r") as f:
    210             for line in f:
    211                 line = line.rstrip()
    212                 line = line.split(",")
    213                 if len(line) == 2:
    214                     overrides[line[0].lower()] = line[1].strip()
    215 
    216         update_lists()
    217 
    218 
    219 pattern = re.compile(r"[A-Z][a-z]*|[a-z]+|\d|[+]")
    220 
    221 # todo: this is garbage
    222 def create_spoken_forms(name, max_len=30):
    223     result = " ".join(list(islice(pattern.findall(name), max_len)))
    224 
    225     result = (
    226         result.replace("0", "zero")
    227         .replace("1", "one")
    228         .replace("2", "two")
    229         .replace("3", "three")
    230         .replace("4", "four")
    231         .replace("5", "five")
    232         .replace("6", "six")
    233         .replace("7", "seven")
    234         .replace("8", "eight")
    235         .replace("9", "nine")
    236         .replace("+", "plus")
    237     )
    238     return result
    239 
    240 
    241 @mod.action_class
    242 class Actions:
    243     def get_running_app(name: str) -> ui.App:
    244         """Get the first available running app with `name`."""
    245         # We should use the capture result directly if it's already in the list
    246         # of running applications. Otherwise, name is from <user.text> and we
    247         # can be a bit fuzzier
    248         if name not in running_application_dict:
    249             if len(name) < 3:
    250                 raise RuntimeError(
    251                     f'Skipped getting app: "{name}" has less than 3 chars.'
    252                 )
    253             for running_name, full_application_name in ctx.lists[
    254                 "self.running"
    255             ].items():
    256                 if running_name == name or running_name.lower().startswith(
    257                     name.lower()
    258                 ):
    259                     name = full_application_name
    260                     break
    261         for app in ui.apps():
    262             if app.name == name and not app.background:
    263                 return app
    264         raise RuntimeError(f'App not running: "{name}"')
    265 
    266     def switcher_focus(name: str):
    267         """Focus a new application by name"""
    268         app = actions.user.get_running_app(name)
    269         app.focus()
    270 
    271         # Hacky solution to do this reliably on Mac.
    272         timeout = 5
    273         t1 = time.monotonic()
    274         if talon.app.platform == "mac":
    275             while ui.active_app() != app and time.monotonic() - t1 < timeout:
    276                 time.sleep(0.1)
    277 
    278     def switcher_launch(path: str):
    279         """Launch a new application by path"""
    280         if app.platform == "windows":
    281             is_valid_path = False
    282             try:
    283                 current_path = Path(path)
    284                 is_valid_path = current_path.is_file()
    285                 # print("valid path: {}".format(is_valid_path))
    286 
    287             except:
    288                 # print("invalid path")
    289                 is_valid_path = False
    290 
    291             if is_valid_path:
    292                 # print("path: " + path)
    293                 ui.launch(path=path)
    294 
    295             else:
    296                 # print("envelop")
    297                 actions.key("super-s")
    298                 actions.sleep("300ms")
    299                 actions.insert("apps: {}".format(path))
    300                 actions.sleep("150ms")
    301                 actions.key("enter")
    302 
    303         else:
    304             ui.launch(path=path)
    305 
    306     def switcher_toggle_running():
    307         """Shows/hides all running applications"""
    308         if gui.showing:
    309             gui.hide()
    310         else:
    311             gui.show()
    312 
    313     def switcher_hide_running():
    314         """Hides list of running applications"""
    315         gui.hide()
    316 
    317 
    318 @imgui.open()
    319 def gui(gui: imgui.GUI):
    320     gui.text("Names of running applications")
    321     gui.line()
    322     for line in ctx.lists["self.running"]:
    323         gui.text(line)
    324 
    325 
    326 def update_launch_list():
    327     launch = {}
    328     if app.platform == "mac":
    329         for base in mac_application_directories:
    330             if os.path.isdir(base):
    331                 for name in os.listdir(base):
    332                     path = os.path.join(base, name)
    333                     name = name.rsplit(".", 1)[0].lower()
    334                     launch[name] = path
    335                     words = name.split(" ")
    336                     for word in words:
    337                         if word and word not in launch:
    338                             if len(name) > 6 and len(word) < 3:
    339                                 continue
    340                             launch[word] = path
    341 
    342     elif app.platform == "windows":
    343         shortcuts = enum_known_folder(FOLDERID_AppsFolder)
    344         # str(shortcuts)
    345         for name in shortcuts:
    346             # print("hit: " + name)
    347             # print(name)
    348             # name = path.rsplit("\\")[-1].split(".")[0].lower()
    349             if "install" not in name:
    350                 spoken_form = create_spoken_forms(name)
    351                 # print(spoken_form)
    352                 launch[spoken_form] = name
    353                 words = spoken_form.split(" ")
    354                 for word in words:
    355                     if word not in words_to_exclude and word not in launch:
    356                         if len(name) > 6 and len(word) < 3:
    357                             continue
    358                         launch[word] = name
    359 
    360     ctx.lists["self.launch"] = launch
    361 
    362 
    363 def ui_event(event, arg):
    364     if event in ("app_launch", "app_close"):
    365         update_lists()
    366 
    367 
    368 # Currently update_launch_list only does anything on mac, so we should make sure
    369 # to initialize user launch to avoid getting "List not found: user.launch"
    370 # errors on other platforms.
    371 ctx.lists["user.launch"] = {}
    372 ctx.lists["user.running"] = {}
    373 
    374 # Talon starts faster if you don't use the `talon.ui` module during launch
    375 def on_ready():
    376     update_overrides(None, None)
    377     fs.watch(overrides_directory, update_overrides)
    378     update_launch_list()
    379     ui.register("", ui_event)
    380 
    381 
    382 # NOTE: please update this from "launch" to "ready" in Talon v0.1.5
    383 app.register("ready", on_ready)