├── .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 |
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
--------------------------------------------------------------------------------