├── .gitignore ├── LICENSE ├── README.md ├── alfred_workflow ├── icon │ └── rf1.png └── workflow │ └── Recent Files.alfredworkflow ├── ignore_files └── example_ignore_file.txt ├── recent_files_alfred_demo.gif └── src └── recent_files /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 jy-gh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Recent Files** 2 | 3 | ## Overview 4 | 5 | **Recent Files** is an [Alfred](https://www.alfredapp.com) workflow that displays a list of recently added/modified files, newest first, in Alfred's file browser. 6 | 7 | The Python script **recent_files**, which does the work behind the workflow, may also be used as a standalone command-line utility; when used as a command-line utility, **recent_files** sends the list of files to standard output. 8 | 9 | ***NOTE***: **Recent Files** was developed on and for macOS. It has not been tested on any other operating system, and the following documentation is written from the perspective of a macOS user. 10 | 11 | ![Demo](recent_files_alfred_demo.gif) 12 | 13 | ## Prerequisites 14 | 15 | ### Alfred Powerpack 16 | 17 | You must have [Alfred's Powerpack](https://www.alfredapp.com/powerpack/) installed in order to use **Recent Files** as an Alfred workflow. 18 | 19 | ### fd 20 | 21 | In addition, the `fd` command is required to run both the **Recent Files** workflow and the **recent_files** command-line utility. Both the **Recent Files** workflow and the **recent_files** command-line utility will attempt to use the `fd` command that is present in your `$PATH`. 22 | 23 | It's possible to confirm that `fd` is installed by entering the following commands in a Terminal window: 24 | 25 | `which fd` 26 | 27 | If the message *fd not found* is displayed it will be necessary to install it in order to use **Recent Files**/**recent_files**. `fd` can be installed from a variety of sources, including: 28 | 29 | * [MacPorts](https://www.macports.org/) 30 | * [Brew](https://brew.sh/) 31 | * [GitHub](https://github.com/sharkdp/fd) 32 | 33 | ### Python 3 34 | 35 | **Recent Files** and **recent_files** require Python 3.8; earlier 3.x versions of Python may work, but have not been tested. 36 | 37 | It's possible to confirm that `python3` is installed by entering the following commands in a Terminal window: 38 | 39 | `which python3` 40 | 41 | If the message *python3 not found* is displayed it will be necessary to install Python in order to use **Recent Files**/**recent_files**. 42 | 43 | Python 3.x may be installed using any of the following: 44 | 45 | * [MacPorts](https://www.macports.org/) 46 | * [Brew](https://brew.sh/) 47 | * [Direct download from Python.org](https://www.python.org/downloads/) 48 | 49 | ## **Recent Files**, the Alfred workflow 50 | 51 | ### Installation 52 | 53 | Download the workflow file and double-click on it to install it. During installation Alfred will display the [Workflow Configuration](#Workflow-Configuration) values screen. 54 | 55 | ### Usage 56 | 57 | `rf` — Invoke **Recent Files** 58 | 59 | After invocation, **Recent Files** will display the list of results in Alfred where any allowed action (open/move/delete/preview, etc.) may be performed. By default, this search starts at the user's `$HOME` directory—although this can be changed, see [Workflow Configuration](#Workflow-Configuration)—and includes files in subdirectories. 60 | 61 | Note that modifier keys (Control, Command, and Option) may be held down to quickly select the following commonly used Alfred actions: 62 | 63 | |Modifier Key|Action| 64 | |---------|--------| 65 | |Command|Reveal File in Finder| 66 | |Option|Open Terminal Here| 67 | |Control|Open with| 68 | 69 | Note that the Shift modifier key is not used, as Alfred can use it to Quick Look the selected item. See Alfred's Preferences (Features -> Previews panel) to enable or disable this feature. 70 | 71 | ### Workflow Configuration 72 | 73 | The behavior of **Recent Files** may be configured by selecting the workflow and clicking on the `Configure Workflow` button in Alfred. Here are the values that can changed from that screen: 74 | 75 | |Variable|Default value|Description| 76 | |---------|--------|--------| 77 | |`keyword`|rf|Keyword to invoke **Recent Files** from Alfred.| 78 | |`FD_COMMAND`|fd|Path to the `fd` command. By default, the workflow will use the `fd` found in the `$PATH` variable.| 79 | |`TOP_LEVEL_DIRECTORY`|~|Top level directory to search from. The ~ (tilde) specifies the user's `$HOME` directory.| 80 | |`IGNORE_FILE`|`example_ignore_file.txt`|File to use as an ignore file; see [Ignore Files](#Ignore-Files).| 81 | |`FILETYPE`|f|This can be set to either **Files only**, **Directories only**, or to **Both files and directories**.| 82 | |`EXTENSIONS`|:|One or more colon-separated file/directory extensions, such as `pdf`, `md`, etc. Dots are not required.| 83 | |`CHANGED_WITHIN`|7d|Show items added/changed within a time period. The default is `7d` (seven days); other units may be used, such as min (minutes), h (hours), and w (weeks). A value of `12h`, for example, would only return files created or modified the previous 12 hours.| 84 | |`MAX_RESULTS`|20|Maximum number of results to display in Alfred.| 85 | |`JSON_DIR_TITLE`|name|If set to **name**, display only the name of the directory as the title (first line) of results. If set to **path**, display the full path to the directory as the title of the results. The subtitle will always display the full **path** to the directory. (Using **name** is less visually cluttered--and more consistent with Alfred's display style.)| 86 | |`JSON_FILE_TITLE`|name|If set to **name**, display only the name of the file as the title (first line) of results. If set to **path**, display the full path to the file as the title of the results. The subtitle will always display the full **path** to the file. (Using **name** is less visually cluttered--and more consistent with Alfred's display style.)| 87 | |`FOLLOW_LINKS`|Ignore|If set to **Ignore**, searches will not traverse symbolically linked directories. If set to **Follow**, symbolically linked directories will be searched for results. Note that it's possible to create a directory structure using symbolic links that would cause the display of duplicate results.| 88 | |`SHOW_HIDDEN`|Ignore|If set to **Ignore**, hidden files (files beginning with the dot ('.') character) will not be displayed. If set to **Show**, hidden files will be displayed in results. Note that if this option is set to **Show** it's possible to display results that would have been ignored by the `IGNORE_FILE`, as the `IGNORE_FILE` has lower precedence to the `fd` command than the hidden files option.| 89 | 90 | ## **recent_files**, the command-line utility 91 | 92 | The **Recent Files** Alfred workflow uses a Python 3 script, **recent_files**, to perform the search. **recent_files** can be used as a standalone command-line utility to display a list of recently-modified files to standard output (stdout). 93 | 94 | ### Installation 95 | 96 | To use **recent_files** as a standalone utility, copy it to a directory in your `$PATH`, such as `~/bin` or wherever user scripts are located. 97 | 98 | ### Command-line usage 99 | 100 | When invoked with `-h`, the following usage message is displayed: 101 | 102 | ``` 103 | usage: recent_files [-h] [-c CHANGED_WITHIN] [-d DIRS] [-e EXTS] 104 | [--fd-command FD_COMMAND] [-H] [-i IGNORE_FILE] 105 | [--json-dir-title {name,path}] 106 | [--json-file-title {name,path}] [-L] [-m MAX_RESULTS] 107 | [-o {text,json}] [--relative-path] [--reverse] 108 | [-t FILETYPES] [--version] 109 | 110 | Finds recent files 111 | 112 | optional arguments: 113 | -h, --help show this help message and exit 114 | -c CHANGED_WITHIN, --changed-within CHANGED_WITHIN 115 | changed within 1h, 2d, 5min, etc.; the default is 7d 116 | -d DIRS, --dir DIRS the directory/directories to search (multiple -d 117 | arguments are allowed); the default is the current 118 | directory 119 | -e EXTS, --extension EXTS 120 | file extensions to search (multiple -e arguments are 121 | allowed); the default is to search all file/directory 122 | extensions 123 | --fd-command FD_COMMAND 124 | path to the fd(1) command if not specified in $PATH 125 | -H, --hidden show hidden files; the default is not to show hidden 126 | files 127 | -i IGNORE_FILE, --ignore-file IGNORE_FILE 128 | path to the Git-format ignore file for search 129 | exclusions, optional 130 | --json-dir-title {name,path} 131 | in JSON (only), display only the directory name as 132 | title; 'path' displays the full pathname to the 133 | directory 134 | --json-file-title {name,path} 135 | in JSON (only), display only the filename as title; 136 | 'path' displays the full pathname 137 | -L, --follow-links Traverse symbolically linked directories; default is 138 | to ignore them 139 | -m MAX_RESULTS, --max-results MAX_RESULTS 140 | only return MAX_RESULTS items; the default is to 141 | return all results 142 | -o {text,json}, --output-format {text,json} 143 | output format, default is text; json is for use by 144 | Alfred 145 | --relative-path display relative file/directory paths; the default is 146 | to display absolute paths 147 | --reverse reverse the sorting order; the default is newest files 148 | first 149 | -t FILETYPES, --filetype FILETYPES 150 | filetype, as supported by fd; the default is "f"; the 151 | argument may be repeated or combined, so both (1) and 152 | (2) are allowed: (1) --filetype fd (2) --filetype f 153 | --filetype d 154 | --version print version information 155 | 156 | ``` 157 | 158 | Note that several of these options are intended for the **Recent Files** Alfred workflow and are probably not useful for command line usage, including `-o`, `--json-dir-title`, and `--json-file-title`. 159 | 160 | ### Command-line examples 161 | 162 | List all files modified within the past day: 163 | 164 | `recent_files --changed-within 1d` 165 | 166 | List files and directories modified within the past three days: 167 | 168 | `recent_files --changed-within 3d --filetype fd` 169 | 170 | List files modified in the past week, in the Documents directory and subdirectories: 171 | 172 | `recent_files -c 1w --dir ~/Documents` 173 | 174 | List `pdf`, `md`, and `txt` files modified in the past day, in the Documents directory and subdirectories (the two commands are equivalent): 175 | 176 | `recent_files -c 1d -e pdf --extension md -e txt --dir ~/Documents` 177 | 178 | `recent_files -c 1d -e pdf:md:txt --dir ~/Documents` 179 | 180 | List files modified in the past week, in the Documents directory, the Downloads directory, and their subdirectories (the two commands are equivalent): 181 | 182 | `recent_files -c 1w --dir ~/Documents --dir ~/Downloads` 183 | 184 | `recent_files -c 1w --dir '~/Documents:~/Downloads'` 185 | 186 | Note that in the second example above single quotes were used to protect the argument to `--dir`; this is especially important if the path contains spaces or shell metacharacters. 187 | 188 | List files modified in the past hour, including hidden files: 189 | 190 | `recent_files -c 1h -H` 191 | 192 | List files modified in the past hour, including files in symbolically linked subdirectories: 193 | 194 | `recent_files -c 1h -L` 195 | 196 | List files modified in the past 30 minutes, using relative paths: 197 | 198 | `recent_files -c 30m --relative-path` 199 | 200 | Use a custom ignore file and display results in reverse order (oldest item first): 201 | 202 | `recent_files --ignore-file ~/my_ignore_file --reverse` 203 | 204 | Specify a different location for the fd command: 205 | 206 | `recent_files --fd-command /some/path/to/fd/command` 207 | 208 | ## Ignore Files 209 | 210 | Both the **Recent Files** workflow and the **recent_files** command-line utility can use a Git-style ignore file. This is used to filter extraneous files from the results. 211 | 212 | This is what the provided ignore file contains: 213 | 214 | ``` 215 | Library 216 | Icon? 217 | ~* 218 | *.photoslibrary 219 | *.musiclibrary 220 | *.tvlibrary 221 | ``` 222 | 223 | This ignore file will cause `fd` to ignore files in the Library folder, Icon files with embedded carriage returns, files beginning with the '~' character (often used for temporary files), and files with `.photoslibrary`, `.musiclibrary`, and `.tvlibrary` extensions. Files matched in an ignore file will not be returned as results unless another option (such as `-H`) takes precedence. 224 | 225 | If this file was named `my_ignore_file` and put in the `$HOME` directory, the following command line would allow **recent_files** to use it: 226 | 227 | `recent_files --ignore-file $HOME/my_ignore_file` 228 | 229 | Note that an empty, 0-length file is a valid ignore file. While this may not be useful for the **recent_files** command-line (since one could simply omit the `-i/--ignore-file` argument) this might be desirable when using the **Recent Files** workflow to return all results without any filtering. 230 | 231 | ## JSON output for Alfred 232 | 233 | **recent_files** produces JSON output format as specified by Alfred. It's unlikely to be useful when using the command-line interface, but this capability is necessary for the **Recent Files** workflow to function correctly. 234 | 235 | See [Script Filter JSON Format](https://www.alfredapp.com/help/workflows/inputs/script-filter/json/) for more, but here's the basic structure: 236 | 237 | ``` 238 | { 239 | "items": [ 240 | { 241 | "type": "file", 242 | "title": "foo.txt", 243 | "subtitle": "/Users/testuser/Documents/foo.txt", 244 | "arg": "/Users/testuser/Documents/foo.txt", 245 | "icon": { 246 | "type": "fileicon", 247 | "path": "/Users/testuser/Documents/foo.txt" 248 | } 249 | }, 250 | { 251 | "type": "file", 252 | "title": "bar.txt", 253 | "arg": "/Users/testuser/Documents/bar.txt" 254 | "subtitle": "/Users/testuser/Documents/bar.txt", 255 | "icon": { 256 | "type": "fileicon", 257 | "path": "/Users/testuser/Documents/bar.txt" 258 | } 259 | } 260 | ] 261 | } 262 | ``` 263 | 264 | Note that **recent_files** does not populate the **uid** and **autocomplete** properties used by Alfred. 265 | 266 | ## Tips on changing search results and speeding up the search 267 | 268 | If **Recent Files**/**recent_files** isn't doing what you need, consider some of the following options: 269 | 270 | ### Change the default changed within time period 271 | 272 | Expanding the time period from the default of 7 days will increase the number of results returned (potentially at the expense of performance), while shrinking the time period will reduce the number of results returned (and perhaps improve performance). 273 | 274 | See the [Workflow Configuration](#Workflow-Configuration) section for information on changing the `CHANGED_WITHIN` value in the **Recent Files** workflow. 275 | 276 | For the **recent_files** command-line utility, do the following: 277 | 278 | Add a `--changed-within` argument to the command line to increase or decrease the default time period to search. 279 | 280 | ### Add a custom ignore file 281 | 282 | A custom ignore file is the best way to filter out unwanted results. 283 | 284 | See the [Workflow Configuration](#Workflow-Configuration) section for information on changing the `IGNORE_FILE` value in the **Recent Files** workflow. 285 | 286 | For the **recent_files** command-line utility, do the following: 287 | 288 | Add the `--ignore-file` argument to the command. See [Ignore Files](#Ignore-Files). 289 | 290 | ### Use multiple directories in the search 291 | 292 | It's sometimes easier to specify the directories to include for searches than it is to filter out directories via [Ignore Files](#Ignore-Files). 293 | 294 | These commands are equivalent: 295 | 296 | ``` 297 | recent_files -c 1w --dir ~/Documents --dir ~/Downloads --dir $HOME/Pictures 298 | # OR 299 | recent_files -c 1w --dir '~/Documents:~/Downloads:$HOME/Pictures' 300 | ``` 301 | 302 | The second version (using the colon-separated list of directories) may also be used in the [Workflow Configuration](#Workflow-Configuration) for the **Recent Files** workflow. 303 | 304 | Note that any environment variable referenced must be defined; this is straightforward when using **recent_files** from the command line, but it's trickier when using the **Recent Files** workflow. ($HOME is a special case that should be defined at all times.) See [Understanding the Scripting Environment](https://www.alfredapp.com/help/workflows/advanced/understanding-scripting-environment/) for an introduction to the issue. 305 | 306 | ### Limit the number of returned results 307 | 308 | See the [Workflow Configuration](#Workflow-Configuration) section for information on changing the `MAX_RESULTS` value in the **Recent Files** workflow. 309 | 310 | For the **recent_files** command-line utility, do the following: 311 | 312 | Add the `--max-results` argument. While this won't increase the performance of the search itself, it will reduce the number of files displayed, which may prevent the list of files from being overly large. 313 | 314 | ### Change the top-level directory 315 | 316 | Changing the default top-level directory from `$HOME` to another directory—such as `$HOME/Documents`—is another way to return results faster, as it will eliminate many files from consideration. 317 | 318 | See the [Workflow Configuration](#Workflow-Configuration) section for information on changing the `TOP_LEVEL_DIRECTORY` value in the **Recent Files** workflow. 319 | 320 | For the **recent_files** command-line utility, do the following: 321 | 322 | Add one or more `--directory` arguments to the command. 323 | 324 | ### See only files/directories with specific extensions 325 | 326 | Add one or more `-e/--extension` arguments to the command. A single, colon-separated argument may be used as well, so `--extension pdf:md:html` is equivalent to `-e pdf --extension md -e html`. 327 | 328 | ## Acknowledgements 329 | 330 | This workflow was inspired by Hans Raaf's Alfred workflow, [*Last changed files*](https://github.com/oderwat/alfredworkflows). 331 | 332 | Thanks to @vitorgalvao for multiple enhancement suggestions. Recent changes, including the `-L` option, processing multiple colon-separated directories, and the changes to the **Recent Files** workflow default display are the result of issues and discussions with @chumido and @saeedesmaili--thanks to both of them. 333 | 334 | ### v0.9.0 335 | 336 | The `-e/--extension` argument, as well as the addition of different Action Modifiers in the Alfred workflow, were prompted by discussions with @NicholasSloan--thanks Nick! 337 | 338 | ### v0.9.1 339 | 340 | Thanks to @NicholasSloan for the new workflow icon! 341 | 342 | ## Copyright 343 | 344 | All code/media is released under the [MIT License](https://opensource.org/licenses/MIT) 345 | -------------------------------------------------------------------------------- /alfred_workflow/icon/rf1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jy-gh/RecentFiles/a2c9529f5f8447614f3e504802875593ba9a57b2/alfred_workflow/icon/rf1.png -------------------------------------------------------------------------------- /alfred_workflow/workflow/Recent Files.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jy-gh/RecentFiles/a2c9529f5f8447614f3e504802875593ba9a57b2/alfred_workflow/workflow/Recent Files.alfredworkflow -------------------------------------------------------------------------------- /ignore_files/example_ignore_file.txt: -------------------------------------------------------------------------------- 1 | Library 2 | Icon? 3 | ~* 4 | *.photoslibrary 5 | *.musiclibrary 6 | *.tvlibrary 7 | -------------------------------------------------------------------------------- /recent_files_alfred_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jy-gh/RecentFiles/a2c9529f5f8447614f3e504802875593ba9a57b2/recent_files_alfred_demo.gif -------------------------------------------------------------------------------- /src/recent_files: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | """ 4 | Shows recently modified files in either JSON or text format. 5 | The JSON format is used for an Alfred workflow. 6 | """ 7 | 8 | import argparse 9 | import io 10 | import json 11 | import os 12 | import re 13 | import subprocess 14 | import sys 15 | 16 | FD_COMMAND = "fd" 17 | FD_DEFAULT_FILETYPE = "f" 18 | FD_ALLOWED_FILETYPES = ("f", "d", "l", "s", "p", "x", "e") 19 | 20 | 21 | class FindRecentFiles: 22 | """Uses the fd command to find recent files""" 23 | 24 | def __init__(self): 25 | """Initialize variables""" 26 | 27 | self._parse_arguments() 28 | self.filetypes = set() 29 | 30 | if self.arguments["filetypes"] is None: 31 | self.filetypes.add(FD_DEFAULT_FILETYPE) 32 | else: 33 | for item in self.arguments["filetypes"]: 34 | for char in item: 35 | if char in FD_ALLOWED_FILETYPES: 36 | self.filetypes.add(char) 37 | else: 38 | print("Error: invalid filetype: '" + char + "'!") 39 | sys.exit(1) 40 | 41 | if self.arguments["fd_command"] is None: 42 | self.arguments["fd_command"] = FD_COMMAND 43 | 44 | if self.arguments["ignore_file"] is not None: 45 | if os.path.isfile(self.arguments["ignore_file"]): 46 | ignore_file = os.path.abspath(self.arguments["ignore_file"]) 47 | self.arguments["ignore_file_absolute_path"] = ignore_file 48 | else: 49 | print("Error: -i/--ignore-file file does not exist!") 50 | print(" " + self.arguments["ignore_file"]) 51 | sys.exit(1) 52 | 53 | def _parse_arguments(self): 54 | """Parses command line arguments""" 55 | parser = argparse.ArgumentParser(description="Finds recent files") 56 | parser.add_argument( 57 | "-c", 58 | "--changed-within", 59 | dest="changed_within", 60 | default="7d", 61 | type=str, 62 | required=False, 63 | help="changed within 1h, 2d, 5min, etc.; the default is 7d", 64 | ) 65 | parser.add_argument( 66 | "-d", 67 | "--dir", 68 | required=False, 69 | dest="dirs", 70 | action='append', 71 | help="the directory/directories to search (multiple -d arguments are allowed); the default is the current directory", 72 | ) 73 | parser.add_argument( 74 | "-e", 75 | "--extension", 76 | required=False, 77 | dest="exts", 78 | action='append', 79 | help="file extensions to search (multiple -e arguments are allowed); the default is to search all file/directory extensions", 80 | ) 81 | parser.add_argument( 82 | "--fd-command", 83 | dest="fd_command", 84 | type=str, 85 | required=False, 86 | help="path to the fd(1) command if not specified in $PATH", 87 | ) 88 | parser.add_argument( 89 | "-H", 90 | "--hidden", 91 | dest="show_hidden", 92 | default=False, 93 | action="store_true", 94 | required=False, 95 | help="show hidden files; the default is not to show hidden files", 96 | ) 97 | parser.add_argument( 98 | "-i", 99 | "--ignore-file", 100 | dest="ignore_file", 101 | type=str, 102 | required=False, 103 | help="path to the Git-format ignore file for search exclusions, optional", 104 | ) 105 | parser.add_argument( 106 | "--json-dir-title", 107 | dest="json_dir_title", 108 | type=str, 109 | default="name", 110 | choices=["name", "path"], 111 | required=False, 112 | help="in JSON (only), display only the directory name as title; 'path' displays the full pathname to the directory", 113 | ) 114 | parser.add_argument( 115 | "--json-file-title", 116 | dest="json_file_title", 117 | type=str, 118 | default="name", 119 | choices=["name", "path"], 120 | required=False, 121 | help="in JSON (only), display only the filename as title; 'path' displays the full pathname", 122 | ) 123 | parser.add_argument( 124 | "-L", 125 | "--follow-links", 126 | dest="follow_links", 127 | default=False, 128 | action="store_true", 129 | required=False, 130 | help="Traverse symbolically linked directories; default is to ignore them", 131 | ) 132 | parser.add_argument( 133 | "-m", 134 | "--max-results", 135 | dest="max_results", 136 | type=int, 137 | default=-1, 138 | required=False, 139 | help="only return MAX_RESULTS items; the default is to return all results", 140 | ) 141 | parser.add_argument( 142 | "-o", 143 | "--output-format", 144 | dest="output_format", 145 | choices=["text", "json"], 146 | required=False, 147 | default="text", 148 | help="output format, default is text; json is for use by Alfred", 149 | ) 150 | parser.add_argument( 151 | "--reverse", 152 | dest="reverse_sort", 153 | default=True, 154 | action="store_false", 155 | required=False, 156 | help="reverse the sorting order; the default is newest files first", 157 | ) 158 | parser.add_argument( 159 | "-t", 160 | "--filetype", 161 | dest="filetypes", 162 | required=False, 163 | action="append", 164 | help="filetype, as supported by fd; the default is \"f\";\ 165 | the argument may be repeated or combined, so both (1) and (2)\ 166 | are allowed:\ 167 | (1) --filetype fd\ 168 | (2) --filetype f --filetype d", 169 | ) 170 | 171 | parsed_arguments = parser.parse_args() 172 | self.arguments = vars(parsed_arguments) 173 | 174 | def _set_options(self): 175 | """Set up the command line options for the fd command""" 176 | options = [] 177 | 178 | options.append("-a") 179 | 180 | options.append("--changed-within") 181 | options.append(self.arguments["changed_within"]) 182 | 183 | for filetype in self.filetypes: 184 | options.append("-t") 185 | options.append(filetype) 186 | 187 | if "ignore_file_absolute_path" in self.arguments: 188 | options.append("--ignore-file") 189 | options.append(self.arguments["ignore_file_absolute_path"]) 190 | 191 | if "show_hidden" in self.arguments and self.arguments["show_hidden"]: 192 | options.append("-H") 193 | 194 | if 'follow_links' in self.arguments and self.arguments["follow_links"]: 195 | options.append("-L") 196 | 197 | if 'exts' in self.arguments and self.arguments['exts'] is not None: 198 | split_exts = [re.split(r':', x) for x in self.arguments['exts']] 199 | if len(split_exts) > 0: 200 | exts = [elem.strip() for y in split_exts for elem in y] 201 | for extension in exts: 202 | if extension != '': 203 | options.append("--extension") 204 | options.append(extension) 205 | 206 | if 'dirs' not in self.arguments or self.arguments['dirs'] is None: 207 | options.append('--search-path') 208 | options.append(os.getcwd()) 209 | else: 210 | split_dirs = [re.split(r':', x) for x in self.arguments['dirs']] 211 | dirlist = [item for sublist in split_dirs for item in sublist] 212 | for dir in dirlist: 213 | options.append('--search-path') 214 | options.append(os.path.expandvars(os.path.expanduser(dir))) 215 | 216 | return options 217 | 218 | def find_recent_files(self): 219 | """Run the fd command, parse the result, and get file mtime values""" 220 | files = {} 221 | options = self._set_options() 222 | 223 | try: 224 | result = subprocess.Popen( 225 | [self.arguments["fd_command"], *options], 226 | stdout=subprocess.PIPE 227 | ) 228 | except FileNotFoundError: 229 | print("Error: the fd command does not exist or is not executable!") 230 | print(" " + self.arguments["fd_command"]) 231 | sys.exit(1) 232 | 233 | for line in io.TextIOWrapper(result.stdout, encoding="utf-8"): 234 | 235 | # Universal newlines are on, and io.TextIOWrapper will translate 236 | # end of line characters to '\n'; however, it seems wise to ensure 237 | # that the line.rstrip() call uses os.linesep, not a hardcoded '\n' 238 | filename = line.rstrip(os.linesep) 239 | 240 | # It makes more sense to simply ignore FileNotFoundError than it 241 | # does to crash the application. 242 | try: 243 | mtime = os.stat(filename).st_mtime 244 | files[filename] = mtime 245 | except FileNotFoundError: 246 | continue 247 | 248 | return files 249 | 250 | @staticmethod 251 | def create_json(sorted_list, max_results, json_dir_title="name", json_file_title="name"): 252 | """Prepare and return JSON output""" 253 | files = [] 254 | result_count = 0 255 | 256 | for list_item in sorted_list: 257 | 258 | if result_count == max_results: 259 | break 260 | 261 | result_count += 1 262 | 263 | filename = list_item[0] 264 | if os.path.isfile(filename): 265 | if json_file_title == 'name': 266 | title = os.path.basename(filename) 267 | else: 268 | title = filename 269 | elif os.path.isdir(filename): 270 | if json_dir_title == 'name': 271 | if filename[-1] == '/': 272 | title = os.path.basename(filename[0:-1]) 273 | else: 274 | title = os.path.basename(filename) 275 | else: 276 | title = filename 277 | else: 278 | title = filename 279 | 280 | record = { 281 | "type": "file", 282 | "title": title, 283 | "subtitle": filename, 284 | "arg": filename, 285 | "icon": {"type": "fileicon", "path": filename}, 286 | } 287 | files.append(record) 288 | 289 | items = {"items": files} 290 | 291 | print(json.dumps(items)) 292 | 293 | @staticmethod 294 | def create_text(sorted_list, max_results): 295 | """Prepare and return plain text output""" 296 | result_count = 0 297 | 298 | for list_item in sorted_list: 299 | 300 | if result_count == max_results: 301 | break 302 | 303 | print(list_item[0]) 304 | result_count += 1 305 | 306 | def run(self): 307 | """Main program""" 308 | 309 | files = self.find_recent_files() 310 | sorted_list = sorted(files.items(), 311 | key=lambda x: x[1], 312 | reverse=self.arguments["reverse_sort"]) 313 | if self.arguments["output_format"] == "json": 314 | self.create_json( 315 | sorted_list, 316 | self.arguments["max_results"], 317 | self.arguments["json_dir_title"], 318 | self.arguments["json_file_title"] 319 | ) 320 | else: 321 | self.create_text(sorted_list, self.arguments["max_results"]) 322 | 323 | 324 | if __name__ == "__main__": 325 | program = FindRecentFiles() 326 | program.run() 327 | --------------------------------------------------------------------------------