├── .DS_Store ├── .gitignore ├── Default App.alfredworkflow ├── README.md ├── img ├── 1.png └── 2.png └── src ├── Alfred3.py ├── apps.py ├── change.py ├── ext.py ├── icon.png ├── info.plist ├── proceed.png └── stop.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/alfred-default-app/04a3c1087a586354c75a0d0193550ae646de2cec/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | 128 | -------------------------------------------------------------------------------- /Default App.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/alfred-default-app/04a3c1087a586354c75a0d0193550ae646de2cec/Default App.alfredworkflow -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Default App for filetype, Alfred 3 2 | 3 | **Default App** changes the assigned app for a specific extension. 4 | 5 | ## Dependencies 6 | 7 | **Default App** uses [duti](https://github.com/moretension/duti) to change assigned apps. 8 | 9 | You need to install *duti* first before you execute the Alfred Workflow 10 | 11 | ```` 12 | brew install duti 13 | ```` 14 | Requires Python 3 15 | 16 | ## Usage 17 | 18 | `dapp` *ext* 19 | 20 | 1. *ext* you can type any extension, the workflow will ask for confirmation to change the assigned app 21 | 2. Choose one of the Apps to assign the extension to. 22 | 23 | ## Screenshots 24 | 25 | ### 1. Step 26 | 27 | ![1](img/1.png) 28 | 29 | ### 2. Step 30 | 31 | ![2](img/2.png) 32 | -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/alfred-default-app/04a3c1087a586354c75a0d0193550ae646de2cec/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/alfred-default-app/04a3c1087a586354c75a0d0193550ae646de2cec/img/2.png -------------------------------------------------------------------------------- /src/Alfred3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | import json 4 | import os 5 | import sys 6 | import time 7 | from plistlib import dump, load 8 | 9 | """ 10 | Alfred Script Filter generator class 11 | Version 4.0 12 | Python 3 required! 13 | """ 14 | 15 | 16 | class Items(object): 17 | """ 18 | Alfred WF Items object to generate Script Filter object 19 | 20 | Returns: 21 | 22 | object: WF object 23 | """ 24 | 25 | def __init__(self): 26 | self.item = {} 27 | self.items = [] 28 | self.mods = {} 29 | 30 | def getItemsLengths(self) -> int: 31 | """ 32 | Get amount of items in object 33 | 34 | Returns: 35 | 36 | int: Number of items 37 | 38 | """ 39 | return len(self.items) 40 | 41 | def setKv(self, key: str, value: str) -> None: 42 | """ 43 | Set a key value pair to item 44 | 45 | Args: 46 | 47 | key (str): Name of the Key 48 | value (str): Value of the Key 49 | """ 50 | self.item.update({key: value}) 51 | 52 | def addItem(self) -> None: 53 | """ 54 | Add/commits an item to the Script Filter Object 55 | 56 | Note: addItem needs to be called after setItem, addMod, setIcon 57 | """ 58 | self.addModsToItem() 59 | self.items.append(self.item) 60 | self.item = {} 61 | self.mods = {} 62 | 63 | def setItem(self, **kwargs: str) -> None: 64 | """ 65 | Add multiple key values to define an item 66 | 67 | Note: addItem needs to be called to submit a Script Filter item 68 | to the Script Filter object 69 | 70 | Args: 71 | 72 | kwargs (kwargs): title,subtitle,arg,valid,quicklookurl,uid,automcomplete,type 73 | """ 74 | for key, value in kwargs.items(): 75 | self.setKv(key, value) 76 | 77 | def getItem(self, d_type: str = "") -> str: 78 | """ 79 | Get current item definition for validation 80 | 81 | Args: 82 | 83 | d_type (str, optional): defines returned object format "JSON" if it needs to be readable . Defaults to "". 84 | 85 | Returns: 86 | 87 | str: JSON represenation of an item 88 | """ 89 | if d_type == "": 90 | return self.item 91 | else: 92 | return json.dumps(self.item, default=str, indent=4) 93 | 94 | def getItems(self, response_type: str = "json") -> json: 95 | """ 96 | get the final items data for which represents the script filter output 97 | 98 | Args: 99 | 100 | response_type (str, optional): "dict"|"json". Defaults to "json". 101 | 102 | Raises: 103 | 104 | ValueError: If key is not "dict"|"json" 105 | 106 | Returns: 107 | 108 | str: returns the item representing script filter output 109 | """ 110 | valid_keys = {"json", "dict"} 111 | if response_type not in valid_keys: 112 | raise ValueError(f"Type must be in: {valid_keys}") 113 | the_items = dict() 114 | the_items.update({"items": self.items}) 115 | if response_type == "dict": 116 | return the_items 117 | elif response_type == "json": 118 | return json.dumps(the_items, default=str, indent=4) 119 | 120 | def setIcon(self, m_path: str, m_type: str = "") -> None: 121 | """ 122 | Set the icon of an item. 123 | Needs to be called before addItem! 124 | 125 | Args: 126 | 127 | m_path (str): Path to the icon 128 | m_type (str, optional): "icon"|"fileicon". Defaults to "". 129 | """ 130 | self.setKv("icon", self.__define_icon(m_path, m_type)) 131 | 132 | def __define_icon(self, path: str, m_type: str = "") -> dict: 133 | """ 134 | Private method to create icon set 135 | 136 | Args: 137 | 138 | path (str): Path to the icon file 139 | 140 | m_type (str, optional): "image"|"fileicon". Defaults to "". 141 | 142 | Returns: 143 | 144 | dict: icon and type 145 | """ 146 | icon = {} 147 | if m_type != "": 148 | icon.update({"type": m_type}) 149 | icon.update({"path": path}) 150 | return icon 151 | 152 | def addMod( 153 | self, 154 | key: str, 155 | arg: str, 156 | subtitle: str, 157 | valid: bool = True, 158 | icon_path: str = "", 159 | icon_type: str = "", 160 | ) -> None: 161 | """ 162 | Add a mod to an item 163 | 164 | Args: 165 | 166 | key (str): "alt"|"cmd"|"shift"|"fn"|"ctrl 167 | arg (str): Value of Mod arg 168 | subtitle (str): Subtitle 169 | valid (bool, optional): Arg valid or not. Defaults to True. 170 | icon_path (str, optional): Path to the icon relative to WF dir. Defaults to "". 171 | icon_type (str, optional): "image"|"fileicon". Defaults to "". 172 | 173 | Raises: 174 | 175 | ValueError: if key is not in list 176 | """ 177 | valid_keys = {"alt", "cmd", "shift", "ctrl", "fn"} 178 | if key not in valid_keys: 179 | raise ValueError(f"Key must be in: {valid_keys}") 180 | mod = {} 181 | mod.update({"arg": arg}) 182 | mod.update({"subtitle": subtitle}) 183 | mod.update({"valid": valid}) 184 | if icon_path != "": 185 | the_icon = self.__define_icon(icon_path, icon_type) 186 | mod.update({"icon": the_icon}) 187 | self.mods.update({key: mod}) 188 | 189 | def addModsToItem(self) -> None: 190 | """ 191 | Adds mod to an item 192 | """ 193 | if bool(self.mods): 194 | self.setKv("mods", self.mods) 195 | self.mods = dict() 196 | 197 | def updateItem(self, id: int, key: str, value: str) -> None: 198 | """ 199 | Update an Alfred script filter item key with a new value 200 | 201 | Args: 202 | 203 | id (int): list indes 204 | key (str): key which needs to be updated 205 | value (str): new value 206 | """ 207 | dict_item = self.items[id] 208 | kv = dict_item[key] 209 | dict_item[key] = kv + value 210 | self.items[id] = dict_item 211 | 212 | def write(self, response_type: str = "json") -> None: 213 | """ 214 | Generate Script Filter Output and write back to stdout 215 | 216 | Args: 217 | 218 | response_type (str, optional): json or dict as output format. Defaults to 'json'. 219 | """ 220 | output = self.getItems(response_type=response_type) 221 | sys.stdout.write(output) 222 | 223 | 224 | class Tools(object): 225 | """ 226 | Alfred Tools, helpful methos when dealing with Scripts in Alfred 227 | 228 | Args: 229 | 230 | object (obj): Object class 231 | """ 232 | @staticmethod 233 | def logPyVersion() -> None: 234 | """ 235 | Log Python Version to shell 236 | """ 237 | Tools.log("PYTHON VERSION:", sys.version) 238 | 239 | @staticmethod 240 | def log(*message) -> None: 241 | """ 242 | Log message to stderr 243 | """ 244 | sys.stderr.write(f'{" ".join(message)}\n') 245 | 246 | @staticmethod 247 | def getEnv(var: str, default: str = str()) -> str: 248 | """ 249 | Reads environment variable 250 | 251 | Args: 252 | 253 | var (string}: Variable name 254 | default (string, optional): fallback if None 255 | 256 | Returns: 257 | 258 | (str): Env value or string if not available 259 | """ 260 | return os.getenv(var) if os.getenv(var) is not None else default 261 | 262 | @staticmethod 263 | def getEnvBool(var: str, default: bool = False) -> bool: 264 | """ 265 | Reads boolean env variable provided as text. 266 | 0 will be treated as False 267 | >1 will be treated as True 268 | 269 | Args: 270 | 271 | var (str): Name of the env variable 272 | default (bool, optional): Default if not found. Defaults to False. 273 | 274 | Returns: 275 | 276 | bool: True or False as bool 277 | """ 278 | if os.getenv(var).isdigit(): 279 | if os.getenv(var) == '0': 280 | return False 281 | else: 282 | return True 283 | if os.getenv(var).lower() == "true": 284 | return True 285 | else: 286 | return default 287 | 288 | @staticmethod 289 | def getArgv(i: int, default=str()) -> str: 290 | """ 291 | Get argument values from input in Alfred or empty if not available 292 | 293 | Args: 294 | 295 | i (int): index of argument 296 | default (string, optional): Fallback if None, default string 297 | 298 | Returns: 299 | 300 | response_type (str) -- argv string or None 301 | """ 302 | try: 303 | return sys.argv[i] 304 | except IndexError: 305 | return default 306 | pass 307 | 308 | @staticmethod 309 | def getDateStr(float_time: float, format: str = "%d.%m.%Y") -> str: 310 | """ 311 | Format float time to string 312 | 313 | Args: 314 | 315 | float_time (float): Time in float 316 | 317 | format (str, optional): format string. Defaults to '%d.%m.%Y'. 318 | 319 | Returns: 320 | 321 | str: Formatted Date String 322 | """ 323 | time_struct = time.gmtime(float_time) 324 | return time.strftime(format, time_struct) 325 | 326 | @staticmethod 327 | def getDateEpoch(float_time: float) -> str: 328 | return time.strftime("%d.%m.%Y", time.gmtime(float_time / 1000)) 329 | 330 | @staticmethod 331 | def sortListDict(list_dict: list, key: str, reverse: bool = True) -> list: 332 | """ 333 | Sort List with Dictionary based on given key in Dict 334 | 335 | Args: 336 | 337 | list_dict (list(dict)): List which contains unsorted dictionaries 338 | 339 | key (str): name of the key of the dict 340 | 341 | reverse (bool, optional): Reverse order. Defaults to True. 342 | 343 | Returns: 344 | 345 | list(dict): sorted list of dictionaries 346 | """ 347 | return sorted(list_dict, key=lambda k: k[key], reverse=reverse) 348 | 349 | @staticmethod 350 | def sortListTuple(list_tuple: list, el: int, reverse: bool = True) -> list: 351 | """ 352 | Sort List with Tubles based on a given element in Tuple 353 | 354 | Args: 355 | 356 | list_tuple (list(tuble)): Sort List with Tubles based on a given element in Tuple 357 | el (int): which element 358 | reverse (bool, optional): Reverse order. Defaults to True. 359 | 360 | Returns: 361 | 362 | list(tuble) -- sorted list with tubles 363 | """ 364 | return sorted(list_tuple, key=lambda tup: tup[el], reverse=reverse) 365 | 366 | @staticmethod 367 | def notify(title: str, text: str) -> None: 368 | """ 369 | Send Notification to mac Notification Center 370 | 371 | Arguments: 372 | 373 | title (str): Title String 374 | text (str): The message 375 | """ 376 | os.system( 377 | f""" 378 | osascript -e 'display notification "{text}" with title "{title}"' 379 | """ 380 | ) 381 | 382 | @staticmethod 383 | def strJoin(*args: str) -> str: 384 | """Joins a list of strings 385 | 386 | Arguments: 387 | 388 | *args (list): List which contains strings 389 | 390 | Returns: 391 | 392 | str: joined str 393 | """ 394 | return str().join(args) 395 | 396 | @staticmethod 397 | def chop(theString: str, ext: str) -> str: 398 | """ 399 | Cuts a string from the end and return the remaining 400 | 401 | Args: 402 | 403 | theString (str): The String to cut 404 | ext (str): String which needs to be removed 405 | 406 | Returns: 407 | 408 | str: chopped string 409 | """ 410 | if theString.endswith(ext): 411 | return theString[: -len(ext)] 412 | return theString 413 | 414 | @staticmethod 415 | def getEnvironment() -> dict: 416 | """ 417 | Get all environment variablse as a dict 418 | 419 | Returns: 420 | 421 | dict: Dict with env variables e.g. {"env1": "value"} 422 | """ 423 | environment = os.environ 424 | env_dict = dict() 425 | for k, v in environment.iteritems(): 426 | env_dict.update({k: v}) 427 | return env_dict 428 | 429 | @staticmethod 430 | def getDataDir() -> str: 431 | data_dir = os.getenv("alfred_workflow_data") 432 | if not (os.path.isdir(data_dir)): 433 | os.mkdir(data_dir) 434 | return data_dir 435 | 436 | @staticmethod 437 | def getCacheDir() -> str: 438 | cache_dir = os.getenv("alfred_workflow_cache") 439 | if not(os.path.isdir(cache_dir)): 440 | os.mkdir(cache_dir) 441 | return cache_dir 442 | 443 | 444 | class Plist: 445 | """ 446 | Plist handling class 447 | 448 | Returns: 449 | 450 | object: A plist object 451 | 452 | 453 | """ 454 | 455 | def __init__(self): 456 | # Read info.plist into a standard Python dictionary 457 | with open("info.plist", "rb") as fp: 458 | self.info = load(fp) 459 | 460 | def getConfig(self) -> str: 461 | return self.info["variables"] 462 | 463 | def getVariable(self, variable: str) -> str: 464 | """ 465 | Get Plist variable with name 466 | 467 | Args: 468 | 469 | variable (str): Name of the variable 470 | 471 | Returns: 472 | 473 | str: Value of variable with name 474 | 475 | """ 476 | try: 477 | return self.info["variables"][variable] 478 | except KeyError: 479 | pass 480 | 481 | def setVariable(self, variable: str, value: str) -> None: 482 | """ 483 | Set a Plist variable 484 | 485 | Args: 486 | 487 | variable (str): Name of Plist Variable 488 | value (str): Value of Plist Variable 489 | 490 | """ 491 | # Set a variable 492 | self.info["variables"][variable] = value 493 | self._saveChanges() 494 | 495 | def deleteVariable(self, variable: str) -> None: 496 | """ 497 | Delete a Plist variable with name 498 | 499 | Args: 500 | 501 | variable (str): Name of the Plist variable 502 | 503 | """ 504 | try: 505 | del self.info["variables"][variable] 506 | self._saveChanges() 507 | except KeyError: 508 | pass 509 | 510 | def _saveChanges(self) -> None: 511 | """ 512 | Save changes to Plist 513 | """ 514 | with open("info.plist", "wb") as fp: 515 | dump(self.info, fp) 516 | 517 | 518 | class Keys(object): 519 | CMD = u'\u2318' 520 | SHIFT = u'\u21E7' 521 | ENTER = u'\u23CE' 522 | ARROW_RIGHT = u'\u2192' 523 | 524 | 525 | class AlfJson(object): 526 | 527 | def __init__(self) -> None: 528 | self.arg: dict = dict() 529 | self.config: dict = dict() 530 | self.variables: dict = dict() 531 | 532 | def add_args(self, d) -> None: 533 | """ 534 | Add arg dictionary 535 | 536 | Args: 537 | 538 | d (dict): Key-Value pairs of args 539 | 540 | """ 541 | self.arg.update(d) 542 | 543 | def add_configs(self, d) -> None: 544 | """ 545 | Add config dictionary 546 | 547 | Args: 548 | 549 | d (dict): Key-Value pairs of configs 550 | 551 | """ 552 | self.config.update(d) 553 | 554 | def add_variables(self, d) -> None: 555 | """ 556 | Add variables dictionary 557 | 558 | Args: 559 | 560 | d (dict): Key-Value pairs of variables 561 | 562 | """ 563 | self.variables.update(d) 564 | 565 | def write_json(self) -> None: 566 | """ 567 | Write Alfred JSON config object to std out 568 | """ 569 | out = {"alfredworkflow": {"arg": self.arg, "config": self.config, "variables": self.variables}} 570 | sys.stdout.write(json.dumps(out)) 571 | -------------------------------------------------------------------------------- /src/apps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/en python3 2 | 3 | import os 4 | import sys 5 | 6 | import Alfred3 as Alfred 7 | 8 | APP_FOLDER = "/Applications" 9 | SYSTEM_APP_FOLDER = "/System/Applications" 10 | 11 | 12 | def get_app_icon(app): 13 | """ 14 | Gets file icon of an app even if the app is one level deeper 15 | 16 | Args: 17 | 18 | app (str): App name without .app 19 | 20 | 21 | Returns: 22 | 23 | str: Absolute Path string 24 | 25 | """ 26 | app_file = os.path.join(APP_FOLDER, f"{app}.app") 27 | sys_app_file = os.path.join(SYSTEM_APP_FOLDER, f"{app}.app") 28 | if os.path.exists(app_file): 29 | return app_file 30 | if os.path.exists(sys_app_file): 31 | return sys_app_file 32 | else: 33 | for root, dirs, files in os.walk(APP_FOLDER): 34 | for dir in dirs: 35 | fi = os.listdir(os.path.join(APP_FOLDER, dir)) 36 | for f in fi: 37 | if app in f: 38 | return os.path.join(APP_FOLDER, dir, f) 39 | 40 | 41 | def app_list(q): 42 | """ Generates as sorted list of App Names in /Applications folder. 43 | The function also covers 1 level in case app is on second level in App folder 44 | 45 | Arguments: 46 | q {string} -- Search String 47 | 48 | Returns: 49 | list -- Sorted list of App Names in /Applications 50 | """ 51 | file_list = os.listdir(APP_FOLDER) 52 | file_list.extend(os.listdir(SYSTEM_APP_FOLDER)) 53 | 54 | for i in file_list: 55 | p = os.path.join(APP_FOLDER, i) 56 | if not (p.endswith(".app")) and os.path.isdir(p): 57 | deep_apps = os.listdir(os.path.join(APP_FOLDER, p)) 58 | for da in deep_apps: 59 | if da.endswith('.app'): 60 | file_list.append(da) 61 | 62 | sorted_file_list = sorted([os.path.splitext(a)[0] for a in file_list if a.endswith('.app')]) 63 | if q != str(): 64 | sorted_file_list = [n for n in sorted_file_list if n.lower().startswith(q.lower())] 65 | return sorted_file_list 66 | 67 | 68 | query = Alfred.Tools.getArgv(1) 69 | wf = Alfred.Items() 70 | apps = app_list(query) 71 | for app in apps: 72 | ax = get_app_icon(app) 73 | wf.setItem( 74 | title=app, 75 | subtitle=f"Continue to assign {app}?", 76 | arg=app 77 | ) 78 | wf.setIcon(ax, 'fileicon') 79 | wf.addItem() 80 | if not app_list(query): 81 | wf.setItem( 82 | title="Nothing Found!", 83 | subtitle=f'No App found beginning with: "{query}"', 84 | valid=False 85 | ) 86 | wf.addItem() 87 | wf.write() 88 | -------------------------------------------------------------------------------- /src/change.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import shutil 5 | import sys 6 | 7 | from Alfred3 import Tools 8 | 9 | 10 | def get_appid(app): 11 | cmd = "osascript -e 'id of app \"" + app + "\"'" 12 | resp = os.popen(cmd).read().splitlines() 13 | resp = resp[0] if len(resp) > 0 else None 14 | return resp 15 | 16 | 17 | def duti(appid, ext): 18 | duti_app = shutil.which('duti') 19 | if duti_app is None: 20 | return False 21 | resp = os.system(f"{duti_app} -s " + appid + " " + ext + " all") 22 | return True if resp == 0 else False 23 | 24 | 25 | query = Tools.getArgv(1) 26 | [app, ext] = query.split('=') 27 | 28 | app_id = get_appid(app) 29 | if app_id is not None: 30 | resp = duti(app_id, ext) 31 | else: 32 | resp = False 33 | 34 | if resp: 35 | sys.stdout.write(f"{ext} succesfully assigned to {app}") 36 | else: 37 | sys.stdout.write(f"Unabled to assign {ext} to {app}") 38 | -------------------------------------------------------------------------------- /src/ext.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | import Alfred3 as Alfred 5 | 6 | 7 | def get_assigned_app(ext): 8 | d_path = get_duti_path() 9 | resp = os.popen(f"{d_path} -x " + ext).read().splitlines() 10 | return resp 11 | 12 | 13 | def get_duti_path() -> str: 14 | paths = ["/usr/local/bin/duti", "/opt/homebrew/bin/duti"] 15 | duti_path = None 16 | for p in paths: 17 | if os.path.isfile(p): 18 | duti_path = p 19 | break 20 | return duti_path 21 | 22 | 23 | ext = Alfred.Tools.getArgv(1) 24 | if not ext.startswith('.'): 25 | ext = '.' + ext 26 | 27 | assigned_app = get_assigned_app(ext) 28 | wf = Alfred.Items() 29 | duti_path = get_duti_path() 30 | if not assigned_app and duti_path: 31 | wf.setItem( 32 | title=f'{ext} is not assigned to an App', 33 | subtitle="Continue?", 34 | arg=ext 35 | ) 36 | wf.setIcon('proceed.png', 'icon') 37 | wf.addItem() 38 | wf.setItem( 39 | title='Don\'t Continue?', 40 | arg='QUIT' 41 | ) 42 | wf.setIcon('stop.png', 'icon') 43 | wf.addItem() 44 | elif duti_path: 45 | wf.setItem( 46 | title=f'\"{ext}\" is assigned to \"{assigned_app[0]}\"', 47 | subtitle='Continue?', 48 | arg=ext 49 | ) 50 | wf.setIcon('proceed.png', 'icon') 51 | wf.addItem() 52 | wf.setItem( 53 | title='Don\'t Continue?', 54 | arg='QUIT' 55 | ) 56 | wf.setIcon('stop.png', 'icon') 57 | wf.addItem() 58 | else: 59 | wf.setItem( 60 | title="Duti was not installed!", 61 | subtitle="Install Duti first: \"brew install duti\"", 62 | valid=False 63 | ) 64 | wf.addItem() 65 | 66 | wf.write() 67 | -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/alfred-default-app/04a3c1087a586354c75a0d0193550ae646de2cec/src/icon.png -------------------------------------------------------------------------------- /src/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | com.apple.alfred.workflow.defaultapp 7 | category 8 | Tools 9 | connections 10 | 11 | 09B1D412-EBF1-43DD-8815-D54D47AF8984 12 | 13 | 14 | destinationuid 15 | E67B33A1-F292-46D5-B2EF-1C7C26C17094 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | 2AF9BA69-5FF8-4BC4-B793-87817A1D2F75 25 | 26 | 27 | destinationuid 28 | 8A92A374-968F-4B02-A028-B59AB3D90C7C 29 | modifiers 30 | 0 31 | modifiersubtext 32 | 33 | vitoclose 34 | 35 | 36 | 37 | 71E4B0EC-CB1F-45AD-999A-5C41F93244DF 38 | 39 | 40 | destinationuid 41 | 9BE1062E-909F-4739-BB3E-B533A9061BB9 42 | modifiers 43 | 0 44 | modifiersubtext 45 | 46 | vitoclose 47 | 48 | 49 | 50 | 75943391-AB2E-4286-A96B-4917B58D57AA 51 | 52 | 53 | destinationuid 54 | 71E4B0EC-CB1F-45AD-999A-5C41F93244DF 55 | modifiers 56 | 0 57 | modifiersubtext 58 | 59 | vitoclose 60 | 61 | 62 | 63 | 8A92A374-968F-4B02-A028-B59AB3D90C7C 64 | 65 | 66 | destinationuid 67 | 75943391-AB2E-4286-A96B-4917B58D57AA 68 | modifiers 69 | 0 70 | modifiersubtext 71 | 72 | vitoclose 73 | 74 | 75 | 76 | A5A9F50F-F445-4D3A-B070-067E88CC9D88 77 | 78 | 79 | destinationuid 80 | 2D608F37-414F-41EF-B397-5A16DD6500D1 81 | modifiers 82 | 0 83 | modifiersubtext 84 | 85 | vitoclose 86 | 87 | 88 | 89 | E67B33A1-F292-46D5-B2EF-1C7C26C17094 90 | 91 | 92 | destinationuid 93 | FE892B16-A265-46DB-ABBD-3C6919E594BB 94 | modifiers 95 | 0 96 | modifiersubtext 97 | 98 | vitoclose 99 | 100 | 101 | 102 | destinationuid 103 | A5A9F50F-F445-4D3A-B070-067E88CC9D88 104 | modifiers 105 | 0 106 | modifiersubtext 107 | 108 | vitoclose 109 | 110 | 111 | 112 | FE892B16-A265-46DB-ABBD-3C6919E594BB 113 | 114 | 115 | destinationuid 116 | 2AF9BA69-5FF8-4BC4-B793-87817A1D2F75 117 | modifiers 118 | 0 119 | modifiersubtext 120 | 121 | vitoclose 122 | 123 | 124 | 125 | 126 | createdby 127 | Acidham 128 | description 129 | Change default app for a filetype 130 | disabled 131 | 132 | name 133 | Default App 134 | objects 135 | 136 | 137 | config 138 | 139 | alfredfiltersresults 140 | 141 | alfredfiltersresultsmatchmode 142 | 0 143 | argumenttreatemptyqueryasnil 144 | 145 | argumenttrimmode 146 | 0 147 | argumenttype 148 | 0 149 | escaping 150 | 102 151 | keyword 152 | dapp 153 | queuedelaycustom 154 | 3 155 | queuedelayimmediatelyinitially 156 | 157 | queuedelaymode 158 | 1 159 | queuemode 160 | 1 161 | runningsubtext 162 | 163 | script 164 | python3 ext.py "$1" 165 | scriptargtype 166 | 1 167 | scriptfile 168 | ext.py 169 | subtext 170 | Enter extension that needs to be reassigned 171 | title 172 | Which extension would you like to change? 173 | type 174 | 5 175 | withspace 176 | 177 | 178 | type 179 | alfred.workflow.input.scriptfilter 180 | uid 181 | 09B1D412-EBF1-43DD-8815-D54D47AF8984 182 | version 183 | 3 184 | 185 | 186 | config 187 | 188 | concurrently 189 | 190 | escaping 191 | 68 192 | script 193 | python3 change.py "$1" 194 | scriptargtype 195 | 1 196 | scriptfile 197 | change.py 198 | type 199 | 5 200 | 201 | type 202 | alfred.workflow.action.script 203 | uid 204 | 71E4B0EC-CB1F-45AD-999A-5C41F93244DF 205 | version 206 | 2 207 | 208 | 209 | config 210 | 211 | lastpathcomponent 212 | 213 | onlyshowifquerypopulated 214 | 215 | removeextension 216 | 217 | text 218 | {query} 219 | title 220 | Change Open With 221 | 222 | type 223 | alfred.workflow.output.notification 224 | uid 225 | 9BE1062E-909F-4739-BB3E-B533A9061BB9 226 | version 227 | 1 228 | 229 | 230 | config 231 | 232 | alfredfiltersresults 233 | 234 | alfredfiltersresultsmatchmode 235 | 0 236 | argumenttreatemptyqueryasnil 237 | 238 | argumenttrimmode 239 | 0 240 | argumenttype 241 | 1 242 | escaping 243 | 102 244 | queuedelaycustom 245 | 3 246 | queuedelayimmediatelyinitially 247 | 248 | queuedelaymode 249 | 0 250 | queuemode 251 | 1 252 | runningsubtext 253 | 254 | script 255 | python3 apps.py "$1" 256 | scriptargtype 257 | 1 258 | scriptfile 259 | apps.py 260 | subtext 261 | Enter name of the App without .app 262 | title 263 | Enter App to open "{var:ext}" 264 | type 265 | 5 266 | withspace 267 | 268 | 269 | type 270 | alfred.workflow.input.scriptfilter 271 | uid 272 | 8A92A374-968F-4B02-A028-B59AB3D90C7C 273 | version 274 | 3 275 | 276 | 277 | config 278 | 279 | inputstring 280 | {query} 281 | matchcasesensitive 282 | 283 | matchmode 284 | 1 285 | matchstring 286 | QUIT 287 | 288 | type 289 | alfred.workflow.utility.filter 290 | uid 291 | FE892B16-A265-46DB-ABBD-3C6919E594BB 292 | version 293 | 1 294 | 295 | 296 | config 297 | 298 | type 299 | 0 300 | 301 | type 302 | alfred.workflow.utility.transform 303 | uid 304 | E67B33A1-F292-46D5-B2EF-1C7C26C17094 305 | version 306 | 1 307 | 308 | 309 | config 310 | 311 | argument 312 | 313 | passthroughargument 314 | 315 | variables 316 | 317 | ext 318 | {query} 319 | 320 | 321 | type 322 | alfred.workflow.utility.argument 323 | uid 324 | 2AF9BA69-5FF8-4BC4-B793-87817A1D2F75 325 | version 326 | 1 327 | 328 | 329 | config 330 | 331 | argument 332 | {query}={var:ext} 333 | passthroughargument 334 | 335 | variables 336 | 337 | 338 | type 339 | alfred.workflow.utility.argument 340 | uid 341 | 75943391-AB2E-4286-A96B-4917B58D57AA 342 | version 343 | 1 344 | 345 | 346 | config 347 | 348 | lastpathcomponent 349 | 350 | onlyshowifquerypopulated 351 | 352 | removeextension 353 | 354 | text 355 | Nothing changed! 356 | title 357 | Canceled 358 | 359 | type 360 | alfred.workflow.output.notification 361 | uid 362 | 2D608F37-414F-41EF-B397-5A16DD6500D1 363 | version 364 | 1 365 | 366 | 367 | config 368 | 369 | inputstring 370 | {query} 371 | matchcasesensitive 372 | 373 | matchmode 374 | 0 375 | matchstring 376 | QUIT 377 | 378 | type 379 | alfred.workflow.utility.filter 380 | uid 381 | A5A9F50F-F445-4D3A-B070-067E88CC9D88 382 | version 383 | 1 384 | 385 | 386 | readme 387 | # Default App for filetype, Alfred 3 388 | 389 | **Default App** changes the assigned app for a specific extension. 390 | 391 | ## Dependencies 392 | 393 | **Default App** uses [duti](https://github.com/moretension/duti) to change assigned apps. 394 | 395 | You need to install *duti* first before you execute the Alfred Workflow: `brew install duti` 396 | 397 | Requires Python 3 398 | 399 | ## Usage 400 | 401 | `dapp` *ext* 402 | 403 | 1. *ext* you can type any extension, the workflow will ask for confirmation to change the assigned app 404 | 2. Choose one of the Apps to assign the extension to. 405 | uidata 406 | 407 | 09B1D412-EBF1-43DD-8815-D54D47AF8984 408 | 409 | note 410 | Enter extension that needs to be changed and get current assignment 411 | xpos 412 | 60 413 | ypos 414 | 50 415 | 416 | 2AF9BA69-5FF8-4BC4-B793-87817A1D2F75 417 | 418 | xpos 419 | 460 420 | ypos 421 | 80 422 | 423 | 2D608F37-414F-41EF-B397-5A16DD6500D1 424 | 425 | xpos 426 | 940 427 | ypos 428 | 190 429 | 430 | 71E4B0EC-CB1F-45AD-999A-5C41F93244DF 431 | 432 | note 433 | Assigns the app to the filetype 434 | xpos 435 | 780 436 | ypos 437 | 50 438 | 439 | 75943391-AB2E-4286-A96B-4917B58D57AA 440 | 441 | xpos 442 | 700 443 | ypos 444 | 80 445 | 446 | 8A92A374-968F-4B02-A028-B59AB3D90C7C 447 | 448 | note 449 | List all available apps under /Applications for new assignment 450 | xpos 451 | 540 452 | ypos 453 | 50 454 | 455 | 9BE1062E-909F-4739-BB3E-B533A9061BB9 456 | 457 | xpos 458 | 940 459 | ypos 460 | 50 461 | 462 | A5A9F50F-F445-4D3A-B070-067E88CC9D88 463 | 464 | xpos 465 | 460 466 | ypos 467 | 220 468 | 469 | E67B33A1-F292-46D5-B2EF-1C7C26C17094 470 | 471 | xpos 472 | 300 473 | ypos 474 | 80 475 | 476 | FE892B16-A265-46DB-ABBD-3C6919E594BB 477 | 478 | xpos 479 | 390 480 | ypos 481 | 80 482 | 483 | 484 | userconfigurationconfig 485 | 486 | variablesdontexport 487 | 488 | version 489 | 1.4.3 490 | webaddress 491 | https://github.com/Acidham/alfred-default-app 492 | 493 | 494 | -------------------------------------------------------------------------------- /src/proceed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/alfred-default-app/04a3c1087a586354c75a0d0193550ae646de2cec/src/proceed.png -------------------------------------------------------------------------------- /src/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/alfred-default-app/04a3c1087a586354c75a0d0193550ae646de2cec/src/stop.png --------------------------------------------------------------------------------