├── .gitignore ├── README.md ├── Recent files in Folder.alfredworkflow ├── rc.png └── src ├── Alfred3.py ├── actions.py ├── attention.png ├── back.png ├── delete.png ├── dropbox.png ├── gear.png ├── icon.png ├── info.plist ├── list_dirs.py ├── list_manager.py ├── main.py ├── purge.png ├── purge.py └── py3.sh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | *.pyc 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recent Files In Folders 2 | 3 | Alfred Workflow to list recent files in folders, newest on top. 4 | 5 | ## NOTE 6 | 7 | **With version 4.0 the configuration was integrated in the Workflow from List Filter. After the upgrade it is required to setup the Folder from scratch!** 8 | **Please ensure to write down (or Screenshot) your current configuration.** 9 | 10 | ## Requires 11 | 12 | * Alfred 5 with Powerpack 13 | * Python 3 14 | 15 | ## Usage 16 | 17 | 1. After the workflow was installed you need to add folders first: 18 | 1. Search for a folder via Alfred 19 | 2. Press `TAB` to enter file action 20 | 1. The `TAB` shortcut is maybe configured differently under: 21 | `Alfred Preferences` → `Features` → `Universal Action` → `Show Actions` 22 | 3. Search for `Add Folder to Recent Folders` and execute 23 | 2. Type `rc` to start Recent Folders Workflow and subsequently the folders will be listed 24 | 3. Press `CMD` to enter addtional actions: 25 | 1. Back to list of folders 26 | 2. Purge Directory to delete all files and folders 27 | 3. Delete folder from configuration 28 | 4. Press `ENTER` to open the underlying file 29 | 5. Press `CMD` to reveal in Finder 30 | 6. Press `Tab` to enter File action on selected file 31 | 32 | ## User Configuration 33 | 34 | * A Custom string format can be set see cheat sheet: http://strftime.org/ 35 | * Search in Subfolders: Subfolders in the directory will be searched as well. If not set Subfolders will be ignored. 36 | * To restrict search to certain filetypes add a comma separated list of extensions e.g. `jpg,png` will only search for jpg and png files. 37 | 38 | ## Screenshot 39 | 40 | rc 41 | 42 | -------------------------------------------------------------------------------- /Recent files in Folder.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/recent-files-in-folders/0bf15eb758b3b31e0d88982185872e025646de85/Recent files in Folder.alfredworkflow -------------------------------------------------------------------------------- /rc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/recent-files-in-folders/0bf15eb758b3b31e0d88982185872e025646de85/rc.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 | from urllib.parse import urlparse 9 | 10 | """ 11 | Alfred Script Filter generator class 12 | Version 4.2 13 | Python 3 required! 14 | """ 15 | 16 | 17 | class Items(object): 18 | """ 19 | Alfred WF Items object to generate Script Filter object 20 | 21 | Returns: 22 | 23 | object: WF object 24 | """ 25 | 26 | def __init__(self): 27 | self.item = {} 28 | self.items = [] 29 | self.mods = {} 30 | 31 | def getItemsLengths(self) -> int: 32 | """ 33 | Get amount of items in object 34 | 35 | Returns: 36 | 37 | int: Number of items 38 | 39 | """ 40 | return len(self.items) 41 | 42 | def setKv(self, key: str, value: str) -> None: 43 | """ 44 | Set a key value pair to item 45 | 46 | Args: 47 | 48 | key (str): Name of the Key 49 | value (str): Value of the Key 50 | """ 51 | self.item.update({key: value}) 52 | 53 | def addItem(self) -> None: 54 | """ 55 | Add/commits an item to the Script Filter Object 56 | 57 | Note: addItem needs to be called after setItem, addMod, setIcon 58 | """ 59 | self.addModsToItem() 60 | self.items.append(self.item) 61 | self.item = {} 62 | self.mods = {} 63 | 64 | def setItem(self, **kwargs: str) -> None: 65 | """ 66 | Add multiple key values to define an item 67 | 68 | Note: addItem needs to be called to submit a Script Filter item 69 | to the Script Filter object 70 | 71 | Args: 72 | 73 | kwargs (kwargs): title,subtitle,arg,valid,quicklookurl,uid,automcomplete,type 74 | """ 75 | for key, value in kwargs.items(): 76 | self.setKv(key, value) 77 | 78 | def getItem(self, d_type: str = "") -> str: 79 | """ 80 | Get current item definition for validation 81 | 82 | Args: 83 | 84 | d_type (str, optional): defines returned object format "JSON" if it needs to be readable . Defaults to "". 85 | 86 | Returns: 87 | 88 | str: JSON represenation of an item 89 | """ 90 | if d_type == "": 91 | return self.item 92 | else: 93 | return json.dumps(self.item, default=str, indent=4) 94 | 95 | def getItems(self, response_type: str = "json") -> json: 96 | """ 97 | get the final items data for which represents the script filter output 98 | 99 | Args: 100 | 101 | response_type (str, optional): "dict"|"json". Defaults to "json". 102 | 103 | Raises: 104 | 105 | ValueError: If key is not "dict"|"json" 106 | 107 | Returns: 108 | 109 | str: returns the item representing script filter output 110 | """ 111 | valid_keys = {"json", "dict"} 112 | if response_type not in valid_keys: 113 | raise ValueError(f"Type must be in: {valid_keys}") 114 | the_items = dict() 115 | the_items.update({"items": self.items}) 116 | if response_type == "dict": 117 | return the_items 118 | elif response_type == "json": 119 | return json.dumps(the_items, default=str, indent=4) 120 | 121 | def setIcon(self, m_path: str, m_type: str = "") -> None: 122 | """ 123 | Set the icon of an item. 124 | Needs to be called before addItem! 125 | 126 | Args: 127 | 128 | m_path (str): Path to the icon 129 | m_type (str, optional): "icon"|"fileicon". Defaults to "". 130 | """ 131 | self.setKv("icon", self.__define_icon(m_path, m_type)) 132 | 133 | def __define_icon(self, path: str, m_type: str = "") -> dict: 134 | """ 135 | Private method to create icon set 136 | 137 | Args: 138 | 139 | path (str): Path to the icon file 140 | 141 | m_type (str, optional): "image"|"fileicon". Defaults to "". 142 | 143 | Returns: 144 | 145 | dict: icon and type 146 | """ 147 | icon = {} 148 | if m_type != "": 149 | icon.update({"type": m_type}) 150 | icon.update({"path": path}) 151 | return icon 152 | 153 | def addMod( 154 | self, 155 | key: str, 156 | arg: str, 157 | subtitle: str, 158 | valid: bool = True, 159 | icon_path: str = "", 160 | icon_type: str = "", 161 | ) -> None: 162 | """ 163 | Add a mod to an item 164 | 165 | Args: 166 | 167 | key (str): "alt"|"cmd"|"shift"|"fn"|"ctrl 168 | arg (str): Value of Mod arg 169 | subtitle (str): Subtitle 170 | valid (bool, optional): Arg valid or not. Defaults to True. 171 | icon_path (str, optional): Path to the icon relative to WF dir. Defaults to "". 172 | icon_type (str, optional): "image"|"fileicon". Defaults to "". 173 | 174 | Raises: 175 | 176 | ValueError: if key is not in list 177 | """ 178 | valid_keys = {"alt", "cmd", "shift", "ctrl", "fn"} 179 | if key not in valid_keys: 180 | raise ValueError(f"Key must be in: {valid_keys}") 181 | mod = {} 182 | mod.update({"arg": arg}) 183 | mod.update({"subtitle": subtitle}) 184 | mod.update({"valid": valid}) 185 | if icon_path != "": 186 | the_icon = self.__define_icon(icon_path, icon_type) 187 | mod.update({"icon": the_icon}) 188 | self.mods.update({key: mod}) 189 | 190 | def addModsToItem(self) -> None: 191 | """ 192 | Adds mod to an item 193 | """ 194 | if bool(self.mods): 195 | self.setKv("mods", self.mods) 196 | self.mods = dict() 197 | 198 | def updateItem(self, id: int, key: str, value: str) -> None: 199 | """ 200 | Update an Alfred script filter item key with a new value 201 | 202 | Args: 203 | 204 | id (int): list indes 205 | key (str): key which needs to be updated 206 | value (str): new value 207 | """ 208 | dict_item = self.items[id] 209 | kv = dict_item[key] 210 | dict_item[key] = kv + value 211 | self.items[id] = dict_item 212 | 213 | def write(self, response_type: str = "json") -> None: 214 | """ 215 | Generate Script Filter Output and write back to stdout 216 | 217 | Args: 218 | 219 | response_type (str, optional): json or dict as output format. Defaults to 'json'. 220 | """ 221 | output = self.getItems(response_type=response_type) 222 | sys.stdout.write(output) 223 | 224 | 225 | class Tools(object): 226 | """ 227 | Alfred Tools, helpful methos when dealing with Scripts in Alfred 228 | 229 | Args: 230 | 231 | object (obj): Object class 232 | """ 233 | @staticmethod 234 | def logPyVersion() -> None: 235 | """ 236 | Log Python Version to shell 237 | """ 238 | Tools.log("PYTHON VERSION:", sys.version) 239 | 240 | @staticmethod 241 | def log(*message) -> None: 242 | """ 243 | Log message to stderr 244 | """ 245 | sys.stderr.write(f'{" ".join(message)}\n') 246 | 247 | @staticmethod 248 | def getEnv(var: str, default: str = str()) -> str: 249 | """ 250 | Reads environment variable 251 | 252 | Args: 253 | 254 | var (string}: Variable name 255 | default (string, optional): fallback if None 256 | 257 | Returns: 258 | 259 | (str): Env value or string if not available 260 | """ 261 | return os.getenv(var) if os.getenv(var) is not None else default 262 | 263 | @staticmethod 264 | def getEnvBool(var: str, default: bool = False) -> bool: 265 | """ 266 | Reads boolean env variable provided as text. 267 | 0 will be treated as False 268 | >1 will be treated as True 269 | 270 | Args: 271 | 272 | var (str): Name of the env variable 273 | default (bool, optional): Default if not found. Defaults to False. 274 | 275 | Returns: 276 | 277 | bool: True or False as bool 278 | """ 279 | try: 280 | if os.getenv(var).isdigit(): 281 | if os.getenv(var) == '0': 282 | return False 283 | else: 284 | return True 285 | if os.getenv(var).lower() == "true": 286 | return True 287 | else: 288 | return default 289 | except AttributeError as e: 290 | sys.exit(f'ERROR: Alfred Environment "{var}" Variable not found!') 291 | 292 | @staticmethod 293 | def getArgv(i: int, default=str()) -> str: 294 | """ 295 | Get argument values from input in Alfred or empty if not available 296 | 297 | Args: 298 | 299 | i (int): index of argument 300 | default (string, optional): Fallback if None, default string 301 | 302 | Returns: 303 | 304 | response_type (str) -- argv string or None 305 | """ 306 | try: 307 | return sys.argv[i] 308 | except IndexError: 309 | return default 310 | pass 311 | 312 | @staticmethod 313 | def getDateStr(float_time: float, format: str = "%d.%m.%Y") -> str: 314 | """ 315 | Format float time to string 316 | 317 | Args: 318 | 319 | float_time (float): Time in float 320 | 321 | format (str, optional): format string. Defaults to '%d.%m.%Y'. 322 | 323 | Returns: 324 | 325 | str: Formatted Date String 326 | """ 327 | time_struct = time.gmtime(float_time) 328 | return time.strftime(format, time_struct) 329 | 330 | @staticmethod 331 | def getDateEpoch(float_time: float) -> str: 332 | return time.strftime("%d.%m.%Y", time.gmtime(float_time / 1000)) 333 | 334 | @staticmethod 335 | def sortListDict(list_dict: list, key: str, reverse: bool = True) -> list: 336 | """ 337 | Sort List with Dictionary based on given key in Dict 338 | 339 | Args: 340 | 341 | list_dict (list(dict)): List which contains unsorted dictionaries 342 | 343 | key (str): name of the key of the dict 344 | 345 | reverse (bool, optional): Reverse order. Defaults to True. 346 | 347 | Returns: 348 | 349 | list(dict): sorted list of dictionaries 350 | """ 351 | return sorted(list_dict, key=lambda k: k[key], reverse=reverse) 352 | 353 | @staticmethod 354 | def sortListTuple(list_tuple: list, el: int, reverse: bool = True) -> list: 355 | """ 356 | Sort List with Tubles based on a given element in Tuple 357 | 358 | Args: 359 | 360 | list_tuple (list(tuble)): Sort List with Tubles based on a given element in Tuple 361 | el (int): which element 362 | reverse (bool, optional): Reverse order. Defaults to True. 363 | 364 | Returns: 365 | 366 | list(tuble) -- sorted list with tubles 367 | """ 368 | return sorted(list_tuple, key=lambda tup: tup[el], reverse=reverse) 369 | 370 | @staticmethod 371 | def notify(title: str, text: str) -> None: 372 | """ 373 | Send Notification to mac Notification Center 374 | 375 | Arguments: 376 | 377 | title (str): Title String 378 | text (str): The message 379 | """ 380 | os.system( 381 | f""" 382 | osascript -e 'display notification "{text}" with title "{title}"' 383 | """ 384 | ) 385 | 386 | @staticmethod 387 | def strJoin(*args: str) -> str: 388 | """Joins a list of strings 389 | 390 | Arguments: 391 | 392 | *args (list): List which contains strings 393 | 394 | Returns: 395 | 396 | str: joined str 397 | """ 398 | return str().join(args) 399 | 400 | @staticmethod 401 | def chop(theString: str, ext: str) -> str: 402 | """ 403 | Cuts a string from the end and return the remaining 404 | 405 | Args: 406 | 407 | theString (str): The String to cut 408 | ext (str): String which needs to be removed 409 | 410 | Returns: 411 | 412 | str: chopped string 413 | """ 414 | if theString.endswith(ext): 415 | return theString[: -len(ext)] 416 | return theString 417 | 418 | @staticmethod 419 | def getEnvironment() -> dict: 420 | """ 421 | Get all environment variablse as a dict 422 | 423 | Returns: 424 | 425 | dict: Dict with env variables e.g. {"env1": "value"} 426 | """ 427 | environment = os.environ 428 | env_dict = dict() 429 | for k, v in environment.iteritems(): 430 | env_dict.update({k: v}) 431 | return env_dict 432 | 433 | @staticmethod 434 | def getDataDir() -> str: 435 | """ 436 | Get Alfred Data Directory 437 | 438 | Returns: 439 | 440 | str: Path to Alfred's data directory 441 | 442 | """ 443 | data_dir = os.getenv("alfred_workflow_data") 444 | if not (os.path.isdir(data_dir)): 445 | os.mkdir(data_dir) 446 | return data_dir 447 | 448 | @staticmethod 449 | def getCacheDir() -> str: 450 | """ 451 | Get Alfreds Cache Directory 452 | 453 | Returns: 454 | 455 | str: path to Alfred's cache directory 456 | 457 | """ 458 | cache_dir = os.getenv("alfred_workflow_cache") 459 | if not (os.path.isdir(cache_dir)): 460 | os.mkdir(cache_dir) 461 | return cache_dir 462 | 463 | @staticmethod 464 | def formatUrl(url: str) -> str: 465 | """ 466 | Format a given string into URL format 467 | 468 | Args: 469 | 470 | url (str): string 471 | 472 | 473 | Returns: 474 | 475 | str: URL string 476 | 477 | """ 478 | if not (url.startswith("http://")) and not (url.startswith("https://")): 479 | url = f"https://{url}" 480 | return url 481 | 482 | @staticmethod 483 | def getDomain(url: str) -> str: 484 | """ 485 | Get Domain of an URL 486 | 487 | Args: 488 | 489 | url (str): string 490 | 491 | 492 | Returns: 493 | 494 | str: URL string 495 | 496 | """ 497 | url = Tools.formatUrl(url) 498 | p = urlparse(url=url) 499 | return f"{p.scheme}://{p.netloc}" 500 | 501 | 502 | class Plist: 503 | """ 504 | Plist handling class 505 | 506 | Returns: 507 | 508 | object: A plist object 509 | 510 | 511 | """ 512 | 513 | def __init__(self): 514 | # Read info.plist into a standard Python dictionary 515 | with open("info.plist", "rb") as fp: 516 | self.info = load(fp) 517 | 518 | def getConfig(self) -> str: 519 | return self.info["variables"] 520 | 521 | def getVariable(self, variable: str) -> str: 522 | """ 523 | Get Plist variable with name 524 | 525 | Args: 526 | 527 | variable (str): Name of the variable 528 | 529 | Returns: 530 | 531 | str: Value of variable with name 532 | 533 | """ 534 | try: 535 | return self.info["variables"][variable] 536 | except KeyError: 537 | pass 538 | 539 | def setVariable(self, variable: str, value: str) -> None: 540 | """ 541 | Set a Plist variable 542 | 543 | Args: 544 | 545 | variable (str): Name of Plist Variable 546 | value (str): Value of Plist Variable 547 | 548 | """ 549 | # Set a variable 550 | self.info["variables"][variable] = value 551 | self._saveChanges() 552 | 553 | def deleteVariable(self, variable: str) -> None: 554 | """ 555 | Delete a Plist variable with name 556 | 557 | Args: 558 | 559 | variable (str): Name of the Plist variable 560 | 561 | """ 562 | try: 563 | del self.info["variables"][variable] 564 | self._saveChanges() 565 | except KeyError: 566 | pass 567 | 568 | def _saveChanges(self) -> None: 569 | """ 570 | Save changes to Plist 571 | """ 572 | with open("info.plist", "wb") as fp: 573 | dump(self.info, fp) 574 | 575 | 576 | class Keys(object): 577 | CMD = u'\u2318' 578 | SHIFT = u'\u21E7' 579 | ENTER = u'\u23CE' 580 | ARROW_RIGHT = u'\u2192' 581 | 582 | 583 | class AlfJson(object): 584 | 585 | def __init__(self) -> None: 586 | """ 587 | Create and write Alfred JSON object 588 | """ 589 | self.arg: dict = dict() 590 | self.config: dict = dict() 591 | self.variables: dict = dict() 592 | 593 | def add_args(self, d: dict) -> None: 594 | """ 595 | Add arg dictionary 596 | 597 | Args: 598 | 599 | d (dict): Key-Value pairs of args 600 | 601 | """ 602 | self.arg.update(d) 603 | 604 | def add_configs(self, d: dict) -> None: 605 | """ 606 | Add config dictionary 607 | 608 | Args: 609 | 610 | d (dict): Key-Value pairs of configs 611 | 612 | """ 613 | self.config.update(d) 614 | 615 | def add_variables(self, d: dict) -> None: 616 | """ 617 | Add variables dictionary 618 | 619 | Args: 620 | 621 | d (dict): Key-Value pairs of variables 622 | 623 | """ 624 | self.variables.update(d) 625 | 626 | def write_json(self) -> None: 627 | """ 628 | Write Alfred JSON config object to std out 629 | """ 630 | out = {"alfredworkflow": {"arg": self.arg, "config": self.config, "variables": self.variables}} 631 | sys.stdout.write(json.dumps(out)) 632 | -------------------------------------------------------------------------------- /src/actions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from Alfred3 import Items, Tools 4 | 5 | target_dir = Tools.getEnv('directory') 6 | 7 | wf = Items() 8 | # Back Item 9 | wf.setItem( 10 | title="BACK", 11 | subtitle="Back to List", 12 | arg='{0}|{1}'.format(target_dir, "BACK") 13 | ) 14 | wf.setIcon( 15 | m_path="back.png", 16 | m_type="image" 17 | ) 18 | wf.addItem() 19 | 20 | # Purge Dir Item 21 | wf.setItem( 22 | title="Purge Directory", 23 | subtitle='Purge "{}"'.format(target_dir), 24 | arg='{0}|{1}'.format(target_dir, "PURGE") 25 | ) 26 | wf.setIcon( 27 | m_path="purge.png", 28 | m_type="image" 29 | ) 30 | wf.addItem() 31 | 32 | # Delete Item 33 | wf.setItem( 34 | title="Remove Folder entry", 35 | subtitle='Remove "{}" from configuration'.format(target_dir), 36 | arg='{0}|{1}'.format(target_dir, "DELETE") 37 | ) 38 | wf.setIcon( 39 | m_path="delete.png", 40 | m_type="image" 41 | ) 42 | wf.addItem() 43 | 44 | wf.write() 45 | -------------------------------------------------------------------------------- /src/attention.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/recent-files-in-folders/0bf15eb758b3b31e0d88982185872e025646de85/src/attention.png -------------------------------------------------------------------------------- /src/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/recent-files-in-folders/0bf15eb758b3b31e0d88982185872e025646de85/src/back.png -------------------------------------------------------------------------------- /src/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/recent-files-in-folders/0bf15eb758b3b31e0d88982185872e025646de85/src/delete.png -------------------------------------------------------------------------------- /src/dropbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/recent-files-in-folders/0bf15eb758b3b31e0d88982185872e025646de85/src/dropbox.png -------------------------------------------------------------------------------- /src/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/recent-files-in-folders/0bf15eb758b3b31e0d88982185872e025646de85/src/gear.png -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/recent-files-in-folders/0bf15eb758b3b31e0d88982185872e025646de85/src/icon.png -------------------------------------------------------------------------------- /src/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | com.apple.alfred.workflow.recentfolder 7 | category 8 | Productivity 9 | connections 10 | 11 | 0BFD10F8-6F17-4B25-B2F3-792E9DF2EF67 12 | 13 | 14 | destinationuid 15 | 6F4F6207-260D-4549-BD57-656FE8F83A92 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | 1F71C1FB-CF5D-4842-86E1-64BF110B74AC 25 | 26 | 27 | destinationuid 28 | 37819932-6353-4D02-89B3-824820362956 29 | modifiers 30 | 0 31 | modifiersubtext 32 | 33 | vitoclose 34 | 35 | 36 | 37 | destinationuid 38 | 1F941EBB-B459-4BFF-8688-97EF827EDC40 39 | modifiers 40 | 0 41 | modifiersubtext 42 | 43 | vitoclose 44 | 45 | 46 | 47 | destinationuid 48 | 0BFD10F8-6F17-4B25-B2F3-792E9DF2EF67 49 | modifiers 50 | 1048576 51 | modifiersubtext 52 | Actions 53 | vitoclose 54 | 55 | 56 | 57 | 37819932-6353-4D02-89B3-824820362956 58 | 59 | 60 | destinationuid 61 | 6DE34E8D-EED5-43C5-855E-017EF2D0E2A0 62 | modifiers 63 | 0 64 | modifiersubtext 65 | 66 | vitoclose 67 | 68 | 69 | 70 | 38F95E2B-A72E-404C-B64E-B8C606E332F5 71 | 72 | 73 | destinationuid 74 | 599D8C67-C857-4846-ACA8-EAA8561F8C57 75 | modifiers 76 | 0 77 | modifiersubtext 78 | 79 | vitoclose 80 | 81 | 82 | 83 | 45C05729-8442-463A-8725-C49A11EC4AF5 84 | 85 | 86 | destinationuid 87 | 453C04B5-A677-4401-A8C4-A6888ED6695D 88 | modifiers 89 | 0 90 | modifiersubtext 91 | 92 | vitoclose 93 | 94 | 95 | 96 | destinationuid 97 | C5BEE472-6561-4A1D-8C91-2B19044D889B 98 | modifiers 99 | 0 100 | modifiersubtext 101 | 102 | vitoclose 103 | 104 | 105 | 106 | 599D8C67-C857-4846-ACA8-EAA8561F8C57 107 | 108 | 109 | destinationuid 110 | 453C04B5-A677-4401-A8C4-A6888ED6695D 111 | modifiers 112 | 0 113 | modifiersubtext 114 | 115 | sourceoutputuid 116 | 6ED80BCA-7105-4B0C-9A6A-82374BCD3537 117 | vitoclose 118 | 119 | 120 | 121 | destinationuid 122 | C6FC6DEA-30CE-442F-995F-2AE1A6937417 123 | modifiers 124 | 0 125 | modifiersubtext 126 | 127 | sourceoutputuid 128 | F0D33E5E-2556-4E28-AAFE-1B0442229538 129 | vitoclose 130 | 131 | 132 | 133 | destinationuid 134 | DFE321CC-443D-4A96-866A-952300D0EC5E 135 | modifiers 136 | 0 137 | modifiersubtext 138 | 139 | vitoclose 140 | 141 | 142 | 143 | 6DE34E8D-EED5-43C5-855E-017EF2D0E2A0 144 | 145 | 146 | destinationuid 147 | A8B78260-5716-406D-8CF8-C79829069297 148 | modifiers 149 | 0 150 | modifiersubtext 151 | 152 | vitoclose 153 | 154 | 155 | 156 | destinationuid 157 | 260F3E19-C40B-4B29-AA96-82B0C2FC36D4 158 | modifiers 159 | 524288 160 | modifiersubtext 161 | Share via Dropbox... 162 | vitoclose 163 | 164 | 165 | 166 | destinationuid 167 | B542D53B-E276-46CD-B6F3-358D5B45DE8C 168 | modifiers 169 | 1048576 170 | modifiersubtext 171 | Reveal in Finder.... 172 | vitoclose 173 | 174 | 175 | 176 | 6F4F6207-260D-4549-BD57-656FE8F83A92 177 | 178 | 179 | destinationuid 180 | 38F95E2B-A72E-404C-B64E-B8C606E332F5 181 | modifiers 182 | 0 183 | modifiersubtext 184 | 185 | vitoclose 186 | 187 | 188 | 189 | 802129CB-E9C7-4E2F-9C6B-88B2B9534A99 190 | 191 | 192 | destinationuid 193 | 1F71C1FB-CF5D-4842-86E1-64BF110B74AC 194 | modifiers 195 | 0 196 | modifiersubtext 197 | 198 | vitoclose 199 | 200 | 201 | 202 | A63455C1-BD98-4E59-A382-6824795C4A56 203 | 204 | 205 | destinationuid 206 | 036D93A8-AB80-4DE7-9890-D04B3B788856 207 | modifiers 208 | 0 209 | modifiersubtext 210 | 211 | vitoclose 212 | 213 | 214 | 215 | A8B78260-5716-406D-8CF8-C79829069297 216 | 217 | 218 | destinationuid 219 | 453C04B5-A677-4401-A8C4-A6888ED6695D 220 | modifiers 221 | 0 222 | modifiersubtext 223 | 224 | sourceoutputuid 225 | DC709C93-D31C-4286-9AE4-384885DB46D9 226 | vitoclose 227 | 228 | 229 | 230 | destinationuid 231 | A74534B6-0DB6-424E-AA7C-556A35206EF0 232 | modifiers 233 | 0 234 | modifiersubtext 235 | 236 | vitoclose 237 | 238 | 239 | 240 | AA6FDC26-BD88-4520-81EE-3A7C9C527CDC 241 | 242 | 243 | destinationuid 244 | 45C05729-8442-463A-8725-C49A11EC4AF5 245 | modifiers 246 | 0 247 | modifiersubtext 248 | 249 | vitoclose 250 | 251 | 252 | 253 | C6FC6DEA-30CE-442F-995F-2AE1A6937417 254 | 255 | 256 | destinationuid 257 | A63455C1-BD98-4E59-A382-6824795C4A56 258 | modifiers 259 | 0 260 | modifiersubtext 261 | 262 | vitoclose 263 | 264 | 265 | 266 | DFE321CC-443D-4A96-866A-952300D0EC5E 267 | 268 | 269 | destinationuid 270 | 45C05729-8442-463A-8725-C49A11EC4AF5 271 | modifiers 272 | 0 273 | modifiersubtext 274 | 275 | vitoclose 276 | 277 | 278 | 279 | 280 | createdby 281 | Acidham 282 | description 283 | Recent created files in a Folder 284 | disabled 285 | 286 | name 287 | Recent files in Folder 288 | objects 289 | 290 | 291 | config 292 | 293 | externaltriggerid 294 | *rc 295 | passinputasargument 296 | 297 | passvariables 298 | 299 | workflowbundleid 300 | self 301 | 302 | type 303 | alfred.workflow.output.callexternaltrigger 304 | uid 305 | 453C04B5-A677-4401-A8C4-A6888ED6695D 306 | version 307 | 1 308 | 309 | 310 | config 311 | 312 | alfredfiltersresults 313 | 314 | alfredfiltersresultsmatchmode 315 | 0 316 | argumenttreatemptyqueryasnil 317 | 318 | argumenttrimmode 319 | 0 320 | argumenttype 321 | 2 322 | escaping 323 | 68 324 | queuedelaycustom 325 | 3 326 | queuedelayimmediatelyinitially 327 | 328 | queuedelaymode 329 | 0 330 | queuemode 331 | 1 332 | runningsubtext 333 | collecting... 334 | script 335 | ./py3.sh main.py "$1" 336 | scriptargtype 337 | 1 338 | scriptfile 339 | main.py 340 | subtext 341 | 342 | title 343 | 344 | type 345 | 5 346 | withspace 347 | 348 | 349 | type 350 | alfred.workflow.input.scriptfilter 351 | uid 352 | 6DE34E8D-EED5-43C5-855E-017EF2D0E2A0 353 | version 354 | 3 355 | 356 | 357 | config 358 | 359 | availableviaurlhandler 360 | 361 | triggerid 362 | *rc 363 | 364 | type 365 | alfred.workflow.trigger.external 366 | uid 367 | 802129CB-E9C7-4E2F-9C6B-88B2B9534A99 368 | version 369 | 1 370 | 371 | 372 | config 373 | 374 | alfredfiltersresults 375 | 376 | alfredfiltersresultsmatchmode 377 | 0 378 | argumenttreatemptyqueryasnil 379 | 380 | argumenttrimmode 381 | 0 382 | argumenttype 383 | 1 384 | escaping 385 | 102 386 | keyword 387 | rc 388 | queuedelaycustom 389 | 3 390 | queuedelayimmediatelyinitially 391 | 392 | queuedelaymode 393 | 0 394 | queuemode 395 | 1 396 | runningsubtext 397 | 398 | script 399 | ./py3.sh list_dirs.py "$1" 400 | scriptargtype 401 | 1 402 | scriptfile 403 | list_dirs.py 404 | subtext 405 | List Recent Files in Folders 406 | title 407 | Recent Files in Folders 408 | type 409 | 5 410 | withspace 411 | 412 | 413 | type 414 | alfred.workflow.input.scriptfilter 415 | uid 416 | 1F71C1FB-CF5D-4842-86E1-64BF110B74AC 417 | version 418 | 3 419 | 420 | 421 | config 422 | 423 | conditions 424 | 425 | 426 | inputstring 427 | 428 | matchcasesensitive 429 | 430 | matchmode 431 | 0 432 | matchstring 433 | *RESTART* 434 | outputlabel 435 | restart 436 | uid 437 | DC709C93-D31C-4286-9AE4-384885DB46D9 438 | 439 | 440 | elselabel 441 | else 442 | hideelse 443 | 444 | 445 | type 446 | alfred.workflow.utility.conditional 447 | uid 448 | A8B78260-5716-406D-8CF8-C79829069297 449 | version 450 | 1 451 | 452 | 453 | config 454 | 455 | argument 456 | 457 | passthroughargument 458 | 459 | variables 460 | 461 | directory 462 | {query} 463 | 464 | 465 | type 466 | alfred.workflow.utility.argument 467 | uid 468 | 37819932-6353-4D02-89B3-824820362956 469 | version 470 | 1 471 | 472 | 473 | config 474 | 475 | openwith 476 | 477 | sourcefile 478 | {query} 479 | 480 | type 481 | alfred.workflow.action.openfile 482 | uid 483 | A74534B6-0DB6-424E-AA7C-556A35206EF0 484 | version 485 | 3 486 | 487 | 488 | config 489 | 490 | externaltriggerid 491 | *drop 492 | passinputasargument 493 | 494 | passvariables 495 | 496 | workflowbundleid 497 | com.apple.alfred.workflow.drop 498 | 499 | type 500 | alfred.workflow.output.callexternaltrigger 501 | uid 502 | 260F3E19-C40B-4B29-AA96-82B0C2FC36D4 503 | version 504 | 1 505 | 506 | 507 | config 508 | 509 | concurrently 510 | 511 | escaping 512 | 0 513 | script 514 | # THESE VARIABLES MUST BE SET. SEE THE ONEUPDATER README FOR AN EXPLANATION OF EACH. 515 | readonly remote_info_plist="https://raw.githubusercontent.com/Acidham/recent-files-in-folders/master/src/info.plist" 516 | readonly workflow_url="https://raw.githubusercontent.com/Acidham/recent-files-in-folders/master/Recent%20files%20in%20Folder.alfredworkflow" 517 | readonly download_type='direct' 518 | readonly frequency_check='4' 519 | 520 | # FROM HERE ON, CODE SHOULD BE LEFT UNTOUCHED! 521 | function abort { 522 | echo "${1}" >&2 523 | exit 1 524 | } 525 | 526 | function url_exists { 527 | curl --silent --location --output /dev/null --fail --range 0-0 "${1}" 528 | } 529 | 530 | function notification { 531 | readonly local notificator="$(find . -type d -name 'Notificator.app')" 532 | if [[ -n "${notificator}" ]]; then 533 | "${notificator}/Contents/Resources/Scripts/notificator" --message "${1}" --title "${alfred_workflow_name}" --subtitle 'A new version is available' 534 | return 535 | fi 536 | 537 | readonly local terminal_notifier="$(find . -type f -name 'terminal-notifier')" 538 | if [[ -n "${terminal_notifier}" ]]; then 539 | "${terminal_notifier}" -title "${alfred_workflow_name}" -subtitle 'A new version is available' -message "${1}" 540 | return 541 | fi 542 | 543 | osascript -e "display notification \"${1}\" with title \"${alfred_workflow_name}\" subtitle \"A new version is available\"" 544 | } 545 | 546 | # Local sanity checks 547 | readonly local_info_plist='info.plist' 548 | readonly local_version="$(/usr/libexec/PlistBuddy -c 'print version' "${local_info_plist}")" 549 | 550 | [[ -n "${local_version}" ]] || abort 'You need to set a workflow version in the configuration sheet.' 551 | [[ "${download_type}" =~ ^(direct|page|github_release)$ ]] || abort "'download_type' (${download_type}) needs to be one of 'direct', 'page', or 'github_release'." 552 | [[ "${frequency_check}" =~ ^[0-9]+$ ]] || abort "'frequency_check' (${frequency_check}) needs to be a number." 553 | 554 | # Check for updates 555 | if [[ $(find "${local_info_plist}" -mtime +"${frequency_check}"d) ]]; then 556 | if ! url_exists "${remote_info_plist}"; then abort "'remote_info_plist' (${remote_info_plist}) appears to not be reachable."; fi # Remote sanity check 557 | 558 | readonly tmp_file="$(mktemp)" 559 | curl --silent --location --output "${tmp_file}" "${remote_info_plist}" 560 | readonly remote_version="$(/usr/libexec/PlistBuddy -c 'print version' "${tmp_file}")" 561 | 562 | if [[ "${local_version}" == "${remote_version}" ]]; then 563 | touch "${local_info_plist}" # Reset timer by touching local file 564 | exit 0 565 | fi 566 | 567 | if [[ "${download_type}" == 'page' ]]; then 568 | notification 'Opening download page…' 569 | open "${workflow_url}" 570 | exit 0 571 | fi 572 | 573 | download_url="$([[ "${download_type}" == 'github_release' ]] && curl --silent "https://api.github.com/repos/${workflow_url}/releases/latest" | grep 'browser_download_url' | head -1 | sed -E 's/.*browser_download_url": "(.*)"/\1/' || echo "${workflow_url}")" 574 | 575 | if url_exists "${download_url}"; then 576 | notification 'Downloading and installing…' 577 | curl --silent --location --output "${HOME}/Downloads/${alfred_workflow_name}.alfredworkflow" "${download_url}" 578 | open "${HOME}/Downloads/${alfred_workflow_name}.alfredworkflow" 579 | else 580 | abort "'workflow_url' (${download_url}) appears to not be reachable." 581 | fi 582 | fi 583 | scriptargtype 584 | 1 585 | scriptfile 586 | 587 | type 588 | 0 589 | 590 | type 591 | alfred.workflow.action.script 592 | uid 593 | 1F941EBB-B459-4BFF-8688-97EF827EDC40 594 | version 595 | 2 596 | 597 | 598 | config 599 | 600 | path 601 | 602 | 603 | type 604 | alfred.workflow.action.revealfile 605 | uid 606 | B542D53B-E276-46CD-B6F3-358D5B45DE8C 607 | version 608 | 1 609 | 610 | 611 | config 612 | 613 | alfredfiltersresults 614 | 615 | alfredfiltersresultsmatchmode 616 | 0 617 | argumenttreatemptyqueryasnil 618 | 619 | argumenttrimmode 620 | 0 621 | argumenttype 622 | 2 623 | escaping 624 | 102 625 | queuedelaycustom 626 | 3 627 | queuedelayimmediatelyinitially 628 | 629 | queuedelaymode 630 | 0 631 | queuemode 632 | 1 633 | runningsubtext 634 | 635 | script 636 | ./py3.sh actions.py "$1" 637 | scriptargtype 638 | 1 639 | scriptfile 640 | actions.py 641 | subtext 642 | 643 | title 644 | 645 | type 646 | 5 647 | withspace 648 | 649 | 650 | type 651 | alfred.workflow.input.scriptfilter 652 | uid 653 | 6F4F6207-260D-4549-BD57-656FE8F83A92 654 | version 655 | 3 656 | 657 | 658 | config 659 | 660 | concurrently 661 | 662 | escaping 663 | 102 664 | script 665 | ./py3.sh purge.py "$1" 666 | scriptargtype 667 | 1 668 | scriptfile 669 | purge.py 670 | type 671 | 5 672 | 673 | type 674 | alfred.workflow.action.script 675 | uid 676 | A63455C1-BD98-4E59-A382-6824795C4A56 677 | version 678 | 2 679 | 680 | 681 | config 682 | 683 | lastpathcomponent 684 | 685 | onlyshowifquerypopulated 686 | 687 | removeextension 688 | 689 | text 690 | {query} 691 | title 692 | Folder was purged! 693 | 694 | type 695 | alfred.workflow.output.notification 696 | uid 697 | 036D93A8-AB80-4DE7-9890-D04B3B788856 698 | version 699 | 1 700 | 701 | 702 | config 703 | 704 | conditions 705 | 706 | 707 | inputstring 708 | {var:action2} 709 | matchcasesensitive 710 | 711 | matchmode 712 | 0 713 | matchstring 714 | BACK 715 | outputlabel 716 | BACK 717 | uid 718 | 6ED80BCA-7105-4B0C-9A6A-82374BCD3537 719 | 720 | 721 | inputstring 722 | {var:action2} 723 | matchcasesensitive 724 | 725 | matchmode 726 | 0 727 | matchstring 728 | PURGE 729 | outputlabel 730 | PURGE 731 | uid 732 | F0D33E5E-2556-4E28-AAFE-1B0442229538 733 | 734 | 735 | elselabel 736 | DELETE 737 | hideelse 738 | 739 | 740 | type 741 | alfred.workflow.utility.conditional 742 | uid 743 | 599D8C67-C857-4846-ACA8-EAA8561F8C57 744 | version 745 | 1 746 | 747 | 748 | config 749 | 750 | argument 751 | 752 | passthroughargument 753 | 754 | variables 755 | 756 | directory 757 | {query} 758 | 759 | 760 | type 761 | alfred.workflow.utility.argument 762 | uid 763 | 0BFD10F8-6F17-4B25-B2F3-792E9DF2EF67 764 | version 765 | 1 766 | 767 | 768 | config 769 | 770 | argument 771 | {var:action1} 772 | passthroughargument 773 | 774 | variables 775 | 776 | 777 | type 778 | alfred.workflow.utility.argument 779 | uid 780 | C6FC6DEA-30CE-442F-995F-2AE1A6937417 781 | version 782 | 1 783 | 784 | 785 | config 786 | 787 | delimiter 788 | | 789 | discardemptyarguments 790 | 791 | outputas 792 | 0 793 | trimarguments 794 | 795 | variableprefix 796 | action 797 | 798 | type 799 | alfred.workflow.utility.split 800 | uid 801 | 38F95E2B-A72E-404C-B64E-B8C606E332F5 802 | version 803 | 1 804 | 805 | 806 | config 807 | 808 | argument 809 | {var:action1}|{var:action2} 810 | passthroughargument 811 | 812 | variables 813 | 814 | 815 | type 816 | alfred.workflow.utility.argument 817 | uid 818 | DFE321CC-443D-4A96-866A-952300D0EC5E 819 | version 820 | 1 821 | 822 | 823 | config 824 | 825 | lastpathcomponent 826 | 827 | onlyshowifquerypopulated 828 | 829 | removeextension 830 | 831 | text 832 | {query} 833 | title 834 | Recent Files in Folder 835 | 836 | type 837 | alfred.workflow.output.notification 838 | uid 839 | C5BEE472-6561-4A1D-8C91-2B19044D889B 840 | version 841 | 1 842 | 843 | 844 | config 845 | 846 | concurrently 847 | 848 | escaping 849 | 102 850 | script 851 | ./py3.sh list_manager.py "$1" 852 | scriptargtype 853 | 1 854 | scriptfile 855 | list_manager.py 856 | type 857 | 5 858 | 859 | type 860 | alfred.workflow.action.script 861 | uid 862 | 45C05729-8442-463A-8725-C49A11EC4AF5 863 | version 864 | 2 865 | 866 | 867 | config 868 | 869 | acceptsmulti 870 | 0 871 | filetypes 872 | 873 | public.folder 874 | public.symlink 875 | 876 | name 877 | Add Folder to Recent Folders 878 | 879 | type 880 | alfred.workflow.trigger.action 881 | uid 882 | AA6FDC26-BD88-4520-81EE-3A7C9C527CDC 883 | version 884 | 1 885 | 886 | 887 | readme 888 | # Recent Files In Folders 889 | https://github.com/Acidham/recent-files-in-folders 890 | 891 | Alfred Workflow to list recent files in folders, newest on top. 892 | 893 | ## NOTE 894 | 895 | **With version 4.0 the configuration was integrated in the Workflow from List Filter. After the upgrade it is required to setup the Folder from scratch!** 896 | **Please ensure to write down (or Screenshot) your current configuration.** 897 | 898 | ## Requires 899 | 900 | * Alfred 5 with Powerpack 901 | * Python 3 902 | 903 | ## Usage 904 | 905 | 1. After the workflow was installed you need to add folders first: 906 | 1. Search for a folder via Alfred 907 | 2. Press `TAB` to enter file action 908 | 1. The `TAB` shortcut is maybe configured differently under: 909 | `Alfred Preferences` → `Features` → `Universal Action` → `Show Actions` 910 | 3. Search for `Add Folder to Recent Folders` and execute 911 | 2. Type `rc` to start Recent Folders Workflow and subsequently the folders will be listed 912 | 3. Press `CMD` to enter addtional actions: 913 | 1. Back to list of folders 914 | 2. Purge Directory to delete all files and folders 915 | 3. Delete folder from configuration 916 | 4. Press `ENTER` to open the underlying file 917 | 5. Press `CMD` to reveal in Finder 918 | 6. Press `Tab` to enter File action on selected file 919 | 920 | ## User Configuration 921 | 922 | * A Custom string format can be set see cheat sheet: http://strftime.org/ 923 | * Search in Subfolders: Subfolders in the directory will be searched as well. If not set Subfolders will be ignored. 924 | * To restrict search to certain filetypes add a comma separated list of extensions e.g. `jpg,png` will only search for jpg and png files. 925 | uidata 926 | 927 | 036D93A8-AB80-4DE7-9890-D04B3B788856 928 | 929 | xpos 930 | 1310 931 | ypos 932 | 620 933 | 934 | 0BFD10F8-6F17-4B25-B2F3-792E9DF2EF67 935 | 936 | xpos 937 | 525 938 | ypos 939 | 650 940 | 941 | 1F71C1FB-CF5D-4842-86E1-64BF110B74AC 942 | 943 | note 944 | List Directories from configuration 945 | xpos 946 | 220 947 | ypos 948 | 105 949 | 950 | 1F941EBB-B459-4BFF-8688-97EF827EDC40 951 | 952 | colorindex 953 | 12 954 | note 955 | OneUpdater 956 | xpos 957 | 590 958 | ypos 959 | 395 960 | 961 | 260F3E19-C40B-4B29-AA96-82B0C2FC36D4 962 | 963 | xpos 964 | 1085 965 | ypos 966 | 340 967 | 968 | 37819932-6353-4D02-89B3-824820362956 969 | 970 | xpos 971 | 525 972 | ypos 973 | 135 974 | 975 | 38F95E2B-A72E-404C-B64E-B8C606E332F5 976 | 977 | xpos 978 | 770 979 | ypos 980 | 650 981 | 982 | 453C04B5-A677-4401-A8C4-A6888ED6695D 983 | 984 | xpos 985 | 1310 986 | ypos 987 | 100 988 | 989 | 45C05729-8442-463A-8725-C49A11EC4AF5 990 | 991 | note 992 | Edit/Delete Entry 993 | xpos 994 | 1085 995 | ypos 996 | 780 997 | 998 | 599D8C67-C857-4846-ACA8-EAA8561F8C57 999 | 1000 | xpos 1001 | 850 1002 | ypos 1003 | 625 1004 | 1005 | 6DE34E8D-EED5-43C5-855E-017EF2D0E2A0 1006 | 1007 | note 1008 | List folder entries 1009 | xpos 1010 | 590 1011 | ypos 1012 | 105 1013 | 1014 | 6F4F6207-260D-4549-BD57-656FE8F83A92 1015 | 1016 | note 1017 | Action menu: 1018 | 1. Purge 1019 | 2. Delete Entry 1020 | xpos 1021 | 590 1022 | ypos 1023 | 620 1024 | 1025 | 802129CB-E9C7-4E2F-9C6B-88B2B9534A99 1026 | 1027 | xpos 1028 | 30 1029 | ypos 1030 | 105 1031 | 1032 | A63455C1-BD98-4E59-A382-6824795C4A56 1033 | 1034 | note 1035 | Purge Folder Action 1036 | xpos 1037 | 1085 1038 | ypos 1039 | 620 1040 | 1041 | A74534B6-0DB6-424E-AA7C-556A35206EF0 1042 | 1043 | xpos 1044 | 1085 1045 | ypos 1046 | 220 1047 | 1048 | A8B78260-5716-406D-8CF8-C79829069297 1049 | 1050 | xpos 1051 | 925 1052 | ypos 1053 | 130 1054 | 1055 | AA6FDC26-BD88-4520-81EE-3A7C9C527CDC 1056 | 1057 | note 1058 | File Action to add to config 1059 | xpos 1060 | 30 1061 | ypos 1062 | 780 1063 | 1064 | B542D53B-E276-46CD-B6F3-358D5B45DE8C 1065 | 1066 | xpos 1067 | 1085 1068 | ypos 1069 | 475 1070 | 1071 | C5BEE472-6561-4A1D-8C91-2B19044D889B 1072 | 1073 | xpos 1074 | 1310 1075 | ypos 1076 | 780 1077 | 1078 | C6FC6DEA-30CE-442F-995F-2AE1A6937417 1079 | 1080 | xpos 1081 | 980 1082 | ypos 1083 | 650 1084 | 1085 | DFE321CC-443D-4A96-866A-952300D0EC5E 1086 | 1087 | xpos 1088 | 980 1089 | ypos 1090 | 760 1091 | 1092 | 1093 | userconfigurationconfig 1094 | 1095 | 1096 | config 1097 | 1098 | default 1099 | %d.%m.%Y 1100 | placeholder 1101 | %d.%m.%Y 1102 | required 1103 | 1104 | trim 1105 | 1106 | 1107 | description 1108 | 1109 | label 1110 | Date Format 1111 | type 1112 | textfield 1113 | variable 1114 | date_format 1115 | 1116 | 1117 | config 1118 | 1119 | default 1120 | 1121 | placeholder 1122 | png,jpg 1123 | required 1124 | 1125 | trim 1126 | 1127 | 1128 | description 1129 | Comma separated list of file extensions to search for e.g. jpg,png,pdf. Leave empty to search over all filetypes 1130 | label 1131 | File Extensions 1132 | type 1133 | textfield 1134 | variable 1135 | ext_comma_sep 1136 | 1137 | 1138 | config 1139 | 1140 | default 1141 | 1142 | required 1143 | 1144 | text 1145 | Search for files including Subfolders 1146 | 1147 | description 1148 | 1149 | label 1150 | 1151 | type 1152 | checkbox 1153 | variable 1154 | search_subfolders 1155 | 1156 | 1157 | variablesdontexport 1158 | 1159 | version 1160 | 4.2.3 1161 | webaddress 1162 | https://github.com/Acidham/recent-files-in-folders 1163 | 1164 | 1165 | -------------------------------------------------------------------------------- /src/list_dirs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import os 5 | 6 | from Alfred3 import Items, Tools 7 | 8 | query = Tools.getArgv(1) 9 | wf_data_dir = Tools.getDataDir() 10 | config_file = os.path.join(wf_data_dir, "folders.json") 11 | wf = Items() 12 | if os.path.isfile(config_file): 13 | with open(config_file, "r") as f: 14 | config = json.load(f) 15 | # List Folders from folders.json 16 | if len(config.keys()) > 0: 17 | for k, v in config.items(): 18 | if os.path.isdir(v) and (query == str() or k.lower().startswith(query.lower())): 19 | wf.setItem( 20 | title=k, 21 | subtitle=u"\u23CE to list Files or \u2318 for addtional actions", 22 | arg=v 23 | ) 24 | wf.addMod( 25 | key="cmd", 26 | arg=v, 27 | subtitle="Enter Action Menu to purge folder or delete entry", 28 | icon_path='gear.png', 29 | icon_type="image" 30 | ) 31 | wf.addModsToItem() 32 | elif not(os.path.isdir(v)): 33 | wf.setItem( 34 | title=k, 35 | subtitle="Folder not found!", 36 | arg=k 37 | ) 38 | wf.setIcon("attention.png", "image") 39 | wf.addItem() 40 | else: 41 | wf.setItem( 42 | title="Folder configuration is empty", 43 | subtitle="Add folder via File Action", 44 | valid=False 45 | ) 46 | wf.setItem('attention.png', 'image') 47 | wf.addItem() 48 | else: 49 | wf.setItem( 50 | title="Add Folder(s) to config", 51 | subtitle="Your run WF for the first time, please add folder(s) via file action first", 52 | valid=False 53 | ) 54 | wf.setIcon("attention.png", 'image') 55 | wf.addItem() 56 | 57 | wf.write() 58 | -------------------------------------------------------------------------------- /src/list_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import os 5 | import sys 6 | 7 | from Alfred3 import Tools 8 | 9 | wf_data_dir = Tools.getDataDir() 10 | config_file = os.path.join(wf_data_dir, 'folders.json') 11 | if os.path.isfile(config_file): 12 | with open(config_file, "r") as f: 13 | config = json.load(f) 14 | else: 15 | config = dict() 16 | 17 | query = Tools.getArgv(1).split('|') 18 | if len(query) > 1: 19 | target_dir = query[0] 20 | action = query[1] 21 | else: 22 | target_dir = query[0] 23 | action = "ADD" 24 | 25 | 26 | target_name = os.path.basename(target_dir) 27 | 28 | if action == "ADD": 29 | config.update({target_name: target_dir}) 30 | else: 31 | new_config = dict() 32 | for k, v in config.items(): 33 | if k != target_name: 34 | new_config.update({k: v}) 35 | config = new_config 36 | 37 | # Remove config before saving changes 38 | os.path.isfile(config_file) and os.remove(config_file) 39 | with open(config_file, 'w') as f: 40 | f.write(json.dumps(config, indent=2)) 41 | 42 | if os.path.isfile(config_file): 43 | sys.stdout.write("Configuration saved") 44 | else: 45 | sys.stdout.write("Cannot write config file") 46 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import math 4 | import os 5 | 6 | from Alfred3 import Items as Items 7 | from Alfred3 import Tools as Tools 8 | 9 | 10 | class RecentFiles: 11 | 12 | def __init__(self, filter, path=str()): 13 | self.path = path 14 | self.filter = filter 15 | 16 | def getRecentFiles(self, reverse=True): 17 | """ 18 | Get list of files in directory as dict 19 | :param reverse: bool 20 | :return: list(dict()) 21 | """ 22 | err = 0 23 | try: 24 | file_list = os.listdir(self.path) 25 | except OSError as e: 26 | err = e.errno 27 | pass 28 | if err == 0: 29 | seq = list() 30 | for f in file_list: 31 | f_path = "{0}/{1}".format(self.path, f) 32 | f_ext = self._getExt(f) 33 | file_stats = os.stat(f_path) 34 | f_time = file_stats.st_birthtime 35 | f_size = file_stats.st_size 36 | 37 | not (f.startswith('.') or f.endswith('\r')) and seq.append({ 38 | 'filename': f, 39 | 'path': f_path, 40 | 'time': f_time, 41 | 'size': f_size, 42 | 'ext': f_ext 43 | }) 44 | sorted_file_list = sorted( 45 | seq, key=lambda k: k['time'], reverse=reverse) 46 | return self._apply_filter(sorted_file_list) 47 | 48 | def getRecentFilesDeep(self, reverse=True): 49 | """ 50 | Get list of files in directory as dict 51 | :param reverse: bool 52 | :return: list(dict()) 53 | """ 54 | err = 0 55 | try: 56 | file_list = os.walk(self.path, topdown=False) 57 | except OSError as e: 58 | err = e.errno 59 | pass 60 | if err == 0: 61 | seq = list() 62 | for root, dirs, files in file_list: 63 | for name in files: 64 | f_path = os.path.join(root, name) 65 | f = os.path.basename(f_path) 66 | if os.path.isfile(f_path) and not (f.startswith('.') or f.endswith('\r')): 67 | f_ext = self._getExt(name) 68 | file_stats = os.stat(f_path) 69 | f_time = file_stats.st_birthtime 70 | f_size = file_stats.st_size 71 | 72 | seq.append({ 73 | 'filename': f, 74 | 'path': f_path, 75 | 'time': f_time, 76 | 'size': f_size, 77 | 'ext': f_ext 78 | }) 79 | 80 | sorted_file_list = sorted(seq, key=lambda k: k['time'], reverse=reverse) 81 | return self._apply_filter(sorted_file_list) 82 | 83 | def _apply_filter(self, file_list_dict): 84 | """ 85 | Apply extension filter to search results. 86 | If empty full file list will be returned 87 | 88 | Args: 89 | 90 | file_list_dict (list): List with file dict 91 | 92 | Returns: 93 | 94 | list: file list dict 95 | """ 96 | seq = list() 97 | if self.filter[0] is not "": 98 | for f in file_list_dict: 99 | if f['ext'] in self.filter: 100 | seq.append(f) 101 | else: 102 | seq = file_list_dict 103 | return seq 104 | 105 | @staticmethod 106 | def _getExt(f_name): 107 | """ 108 | Get file extension from filename 109 | 110 | Args: 111 | 112 | f_name (string): filename 113 | 114 | Returns: 115 | 116 | string: extension without leading dot 117 | """ 118 | f_ext = os.path.splitext(f_name)[1:][0] 119 | ret = f_ext.replace(".", "") 120 | return ret 121 | 122 | @staticmethod 123 | def convertFileSize(size_bytes): 124 | """ 125 | Convert filesize in bytes 126 | :param size_bytes: float() 127 | :return: formatted file size: str() 128 | """ 129 | if size_bytes == 0: 130 | return "0B" 131 | size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") 132 | i = int(math.floor(math.log(size_bytes, 1024))) 133 | p = math.pow(1024, i) 134 | s = round(size_bytes / p, 2) 135 | return "{0} {1}".format(s, size_name[i]) 136 | 137 | @staticmethod 138 | def search(query, dict_list): 139 | """ 140 | Search string in a list of Dict 141 | :param query: str() 142 | :param dict_list: list(dict()) 143 | :return: list(dict()) 144 | """ 145 | seq = list() 146 | for d in dict_list: 147 | # if d['filename'].lower().startswith(query.lower()): 148 | if query.lower() in d['filename'].lower(): 149 | seq.append(d) 150 | return seq 151 | 152 | 153 | # Load Env, Argv and set working path 154 | t_dir = Tools.getEnv('directory') 155 | search_subfolders = True if Tools.getEnvBool('search_subfolders') else False 156 | working_path = t_dir 157 | query = Tools.getArgv(1) 158 | date_format = Tools.getEnv('date_format') 159 | extensions = Tools.getEnv('ext_comma_sep').split(',') 160 | 161 | # Read file list deep and normal 162 | files_in_directory = None 163 | file_list = list() 164 | if working_path: 165 | rf = RecentFiles(extensions, working_path) 166 | files_in_directory = rf.getRecentFilesDeep(reverse=True) if search_subfolders else rf.getRecentFiles(reverse=True) 167 | file_list = RecentFiles.search(query, files_in_directory) if bool( 168 | query) and files_in_directory else files_in_directory 169 | 170 | wf = Items() 171 | 172 | # When path not found, expose error to script filter 173 | if files_in_directory is None: 174 | wf.setItem( 175 | title='Path "{0}" not found...'.format(t_dir), 176 | subtitle='Change path in List Filter, e.g. /Users//Desktop', 177 | valid=False 178 | ) 179 | wf.setIcon('attention.png', 'png') 180 | wf.addItem() 181 | # In case no files were found in directory 182 | elif len(files_in_directory) == 0: 183 | wf.setItem( 184 | title='Folder is empty!', 185 | subtitle=u"\u23CE to start again", 186 | arg="*RESTART*", 187 | valid=True 188 | ) 189 | wf.setIcon('attention.png', 'png') 190 | wf.addItem() 191 | # if file search has no results 192 | elif len(file_list) == 0: 193 | wf.setItem( 194 | title='Cannot find file starting with "{0}"'.format(query), 195 | valid=False 196 | ) 197 | wf.setIcon('attention.png', 'png') 198 | wf.addItem() 199 | # Expose sorted file list to Script Filter 200 | else: 201 | for d in file_list: 202 | path = d['path'] 203 | size = RecentFiles.convertFileSize( 204 | d['size']) if os.path.isfile(path) else '-' 205 | filename = d['filename'] 206 | a_date = Tools.getDateStr(d['time'], date_format) 207 | 208 | wf.setItem( 209 | title=filename, 210 | type='file', 211 | subtitle=u'Added: {0}, Size: {1} (\u2318 Reveal in Finder)'.format( 212 | a_date, size), 213 | arg=path, 214 | quicklookurl=path 215 | ) 216 | wf.setIcon(path, 'fileicon') 217 | wf.addItem() 218 | wf.write() 219 | -------------------------------------------------------------------------------- /src/purge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Acidham/recent-files-in-folders/0bf15eb758b3b31e0d88982185872e025646de85/src/purge.png -------------------------------------------------------------------------------- /src/purge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import shutil 4 | import sys 5 | 6 | from Alfred3 import Tools 7 | 8 | 9 | def remove(p): 10 | if os.path.isfile(p): 11 | os.remove(p) 12 | elif os.path.isdir(p): 13 | shutil.rmtree(p) 14 | elif os.path.islink(p): 15 | os.unlink(p) 16 | 17 | 18 | f_path = Tools.getArgv(1) 19 | 20 | # Purge directory, excl. System files 21 | for it in os.listdir(f_path): 22 | if it != ".DS_Store" and it != "Icon\r": 23 | it_path = os.path.join(f_path, it) 24 | remove(it_path) 25 | sys.stdout.write(f_path) 26 | -------------------------------------------------------------------------------- /src/py3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #Set to 0 to use the first Python3 version found 3 | PREFER_LATEST=1 4 | 5 | #Array of python3 paths 6 | PYPATHS=(/usr/bin /usr/local/bin) 7 | 8 | #Input arguments 9 | SCR="${1}" 10 | QUERY="${2}" 11 | WF_DATA_DIR=$alfred_workflow_data 12 | 13 | #Create wf data dir if not available 14 | [ ! -d "$WF_DATA_DIR" ] && mkdir "$WF_DATA_DIR" 15 | 16 | SCRPATH="$0" 17 | SCRIPT_DIR="$(dirname $SCRPATH)" 18 | 19 | #Cache file for python binary - Allowes for faster execution 20 | PYALIAS="$WF_DATA_DIR/py3" 21 | 22 | CONFIG_PREFIX="Config" 23 | DEBUG=0 24 | 25 | pyrun() { 26 | $py3 "${SCR}" "${QUERY}" 27 | RES=$? 28 | [[ $RES -eq 127 ]] && handle_py_notfound 29 | return $RES 30 | } 31 | 32 | handle_py_notfound() { 33 | #we need this in case of some OS reconfiguration , python3 uninstalled ,etc.. 34 | log_debug "python3 configuration changed, attemping to reconfigure" 35 | setup_python_alias 36 | } 37 | 38 | verify_not_stub() { 39 | PYBIN="${1}" 40 | $PYBIN -V > /dev/null 2>&1 41 | return $? 42 | } 43 | 44 | getver() { 45 | PYBIN="${1}" 46 | #Extract py3 version info and convert to comparable decimal 47 | VER=$($PYBIN -V | cut -f2 -d" " | sed -E 's/\.([0-9]+)$/\1/') 48 | echo $VER 49 | log_debug "Version: $VER" 50 | } 51 | 52 | make_alias() { 53 | PYBIN="${1}" 54 | PYVER="$2" 55 | #last sanitization 56 | [ -z "${PYBIN}" ] && log_msg "Error: invalid python3 path" && exit 255 57 | [ -z "${PYVER}" ] && PYVER="$(getver "$PYBIN")" 58 | echo "export py3='$PYBIN'" > "$PYALIAS" 59 | log_msg "Python3 was found at $PYBIN." "Version: $PYVER, Proceed typing query or re-run worfklow" 60 | } 61 | 62 | log_msg() { 63 | log_json "$CONFIG_PREFIX: $1" "$2" 64 | log_debug "$1" 65 | } 66 | 67 | log_json() { 68 | #need to use json for notifications since we're in script filter 69 | title="$1" 70 | sub="$2" 71 | [ -z "$sub" ] && sub="$title" 72 | cat <&2 86 | } 87 | 88 | setup_python_alias() { 89 | current_py="" 90 | current_ver=0.00 91 | for p in "${PYPATHS[@]}" 92 | do 93 | if [ -f $p/python3 ] 94 | then 95 | #check path does not contain a stub 96 | # set -x 97 | ! verify_not_stub "$p/python3" && continue 98 | #check for latest py3 version 99 | if [ $PREFER_LATEST -eq 1 ] 100 | then 101 | thisver=$(getver $p/python3) 102 | if [[ $(echo "$thisver > $current_ver" | bc -l) -eq 1 ]] 103 | then 104 | current_ver=$thisver 105 | current_py=$p/python3 106 | fi 107 | else 108 | #Just take the first valid python3 found 109 | make_alias "$p/python3" 110 | return 0 111 | fi 112 | fi 113 | done 114 | if [ $current_ver = 0.00 ] 115 | then 116 | log_msg "Error: no valid python3 version found" "Please locate python version and add to PYPATHS variable" 117 | exit 255 118 | fi 119 | make_alias "$current_py" "$current_ver" 120 | . "$PYALIAS" 121 | } 122 | 123 | #Main 124 | if [ -f "$PYALIAS" ] 125 | then 126 | . "$PYALIAS" 127 | pyrun 128 | exit 129 | else 130 | setup_python_alias 131 | fi --------------------------------------------------------------------------------