├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── README_CN.md ├── ccl_bplist.py ├── icon.png ├── info.plist ├── mac_alias ├── __init__.py ├── alias.py ├── bookmark.py ├── osx.py └── utils.py ├── main.py └── pinyin ├── Mandarin.dat ├── __init__.py ├── _compat.py ├── cmd.py └── pinyin.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mpco 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | *.pyc 107 | *.pyo 108 | 109 | # macOS 110 | # General 111 | .DS_Store 112 | .AppleDouble 113 | .LSOverride 114 | 115 | # Thumbnails 116 | ._* 117 | 118 | # Files that might appear in the root of a volume 119 | .DocumentRevisions-V100 120 | .fseventsd 121 | .Spotlight-V100 122 | .TemporaryItems 123 | .Trashes 124 | .VolumeIcon.icns 125 | .com.apple.timemachine.donotpresent 126 | 127 | # Directories potentially created on remote AFP share 128 | .AppleDB 129 | .AppleDesktop 130 | .apdisk 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Charles Ma 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build open 2 | 3 | open: 4 | open ~/Desktop/RecentDocuments.alfredworkflow 5 | 6 | build: 7 | zip -r ~/Desktop/RecentDocuments.alfredworkflow . -x '*.git*' 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Recent Documents / Apps

2 | 3 |

Quickly access recent documents and apps.

4 | 5 |

6 | 7 | macOS 10.11+ 8 | 9 | 10 | 11 | 12 |

13 | 14 |

15 | 中文说明 • 16 | Download • 17 | How To Use • 18 | Configuration 19 |

20 | 21 | **Sorry, no more updates as I no longer use a Mac computer and the installation of macOS virtual machine failed.** 22 | 23 | ## How to Use 24 | 25 | You can press `Enter` to open the file in result, or press `⌘CMD-Enter` to reveal it in Finder. 26 | 27 | ### Tap `rr` to list files opened recently by the foremost app. 28 | 29 | For example: 30 | 31 | - Recent folders will be listed when Finder is foremost. 32 | - Recent rtf, text files will be listed when TextEdit app is foremost. 33 | - Recent *.sketch files will be listed when Sketch app is foremost. 34 | - Recent *.xcodeproj project files will be listed when Xcode app is foremost. 35 | 36 | The subtitle of each result consists of **⏱modified time** and **📡path** of the file. 37 | 38 | ![rr](https://user-images.githubusercontent.com/3690653/45074732-2fda4d00-b117-11e8-87a2-55684819f826.png) 39 | 40 | ### Tap `rf` to list recent folders. 41 | 42 | Opening recent folders is very common in use. Tapping `rf` is a more efficient way, even though you can activate Finder and then tap `rr`. 43 | 44 | ![rf](https://user-images.githubusercontent.com/3690653/45074731-2fda4d00-b117-11e8-8d66-27e9d456fb53.png) 45 | 46 | ### Tap `rd` to list recent files. 47 | 48 | These files were recently opened by user, not like `rr` which is just for the foremost app. 49 | 50 | ![rd](https://user-images.githubusercontent.com/3690653/45074730-2f41b680-b117-11e8-8234-fd377533f396.png) 51 | 52 | ### Tap `ra` to list apps opened recently. 53 | 54 | ![ra](https://user-images.githubusercontent.com/3690653/45076634-7a5ec800-b11d-11e8-9e1c-f16ac17875fb.png) 55 | 56 | ## Configuration 57 | 58 | **Exclude files and folders from the results.** 59 | 60 | You can add private folder paths separated by colon `:` to the workflow environment variable `ExcludedFolders`. The results will not show private folders and any files inside them. The environment variable `ExcludedFiles` is used to block files and folders themselves. 61 | 62 | For example: `~/privateFolder1/:/Users/G/privateFolder2/` 63 | 64 | ### Optional Setup 65 | 66 | 1. Adjust keywords `rr、rf、rd、ra` as you like. 67 | 2. Open `System Preferences - General`, change the number of `Recent items` to 15 or more. 68 | 69 | ## Donation 70 | 71 | Feel free to donate by Wechat if this workflow is helpful. 72 | 73 | ![Wechat Reward Code](https://user-images.githubusercontent.com/3690653/45010129-68f2be80-b03e-11e8-825f-cea7b3853342.JPG) 74 | 75 | ## Dependencies: 76 | 77 | * macMRU-Parser: https://github.com/mac4n6/macMRU-Parser 78 | * ccl_bplist.py: https://github.com/cclgroupltd/ccl-bplist 79 | * mac\_alias: https://pypi.python.org/pypi/mac_alias 80 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Recent Documents / Apps 2 | 3 | 快速打开最近访问的文档、文件夹、应用。 4 | 快速打开当前应用的最近访问文件。 5 | 6 | 系统要求:macOS 10.11+。 7 | 10.11 未测试 8 | 9 | [下载 Workflow](https://github.com/mpco/Alfred3-workflow-recent-documents/releases) 10 | 11 | ## 用法说明 12 | 13 | - 使用`上下方向键`或`Ctrl-Up/Down`或`文件名过滤`选择列表中的结果。 14 | - 对于中文文件名,支持拼音过滤。每个字的拼音之间需加空格。 15 | 如:`爱国教育.mp4`对应过滤语句`ai guo jiao yu .mp4`。[详细说明](https://github.com/mpco/Alfred3-workflow-recent-documents/releases/tag/2.5) 16 | - 按下 回车键 打开搜索结果中的文件。 17 | - 按下 ⌘CMD+回车键 以在访达中显示文件。 18 | 19 | ### 输入`rr`,列出当前激活应用的最近文档。 20 | 21 | 举个栗子🌰️ : 22 | 23 | - 访达(Finder)在最前,则列出最近访问的文件夹。这样就不用再一层层地找**最近访问过**或**刚刚关闭**的文件夹了。 24 | - 文本编辑(TextEdit)在最前,则列出最近打开过的 rtf、txt 文件。 25 | - Sketch 在最前,则列出最近打开过的 Sketch 设计文档。 26 | - Xcode 在最前,则列出最近打开过的 Xcode 工程。 27 | 28 | 每个条目的子标题由该文件的**⏱修改日期**与**📡路径**组成。 29 | 30 | 这个功能本质上是列出应用菜单栏中` 文件 - 打开最近使用 `内的最近文档列表。所以只要应用的菜单栏中存在` 文件 - 打开最近使用 `菜单项,就能正常工作。点击` 文件 - 打开最近使用 `中的`清除菜单` 则可清理列表。清理后,输入`rr`会显示`没有最近文件记录(None Recent Record)`。 31 | 32 | ![rr](https://user-images.githubusercontent.com/3690653/45074732-2fda4d00-b117-11e8-87a2-55684819f826.png) 33 | 34 | ### 输入`rf`,列出最近访问的文件夹。 35 | 36 | 工作过程中,常常需要打开最近访问的文件夹。虽然可以切换至访达而后输入`rr`,但为了更加高效,故额外增加了该功能。 37 | 38 | ![rf](https://user-images.githubusercontent.com/3690653/45074731-2fda4d00-b117-11e8-8d66-27e9d456fb53.png) 39 | 40 | ### 输入`rd`,列出最近打开的各种文件。 41 | 该功能与第一个`rf`的区别在于,`rd`会列出全局的最近文档,而非针对当前应用。 42 | 43 | ![rd](https://user-images.githubusercontent.com/3690653/45074730-2f41b680-b117-11e8-8234-fd377533f396.png) 44 | 45 | ### 输入`ra`,列出最近打开的应用。 46 | 47 | ![ra](https://user-images.githubusercontent.com/3690653/45076634-7a5ec800-b11d-11e8-9e1c-f16ac17875fb.png) 48 | 49 | ### 排除隐私文件夹 50 | 51 | 你可能希望某些文件或文件夹不要出现在结果中,比如 *.avi 之类的。可以在 Workflow 环境变量`ExcludedFolders`中加入以冒号分隔的文件夹路径。这些文件夹以及其中的任何文件都不会出现在结果中。 52 | 53 | 举例:`~/privateFolder1/:/Users/G/privateFolder2/` 54 | 55 | ![excludedfolders](https://user-images.githubusercontent.com/3690653/45142715-c1b38a00-b1eb-11e8-9ace-3abeeb99f425.png) 56 | 57 | ### 可选配置: 58 | 59 | 1. 调整`rr、rf、rd、ra`这些关键词,以更加符合你的习惯或需要。 60 | 2. 打开`系统偏好设置 - 通用`,将`最近使用的项目`调整至 15 个或更多。这是因为默认的数量是 10 个,而最近使用的项目记录中有时存在已删除的文件,该 Workflow 会滤除这些已删除的结果,可能导致显示的结果偏少。 61 | 62 | ## 互助互爱 63 | 64 | 哈哈哈,这个 Workflow 是不是很棒,简直想给自己一个么么哒~ 65 | 如果这个 Workflow 让你感到很好用,请慷慨赞助(微信扫码)。 66 | 67 | ![微信赞赏码](https://user-images.githubusercontent.com/3690653/45010129-68f2be80-b03e-11e8-825f-cea7b3853342.JPG) 68 | 69 | 70 | 71 | ## 依赖项目 72 | 73 | * macMRU-Parser: https://github.com/mac4n6/macMRU-Parser 74 | * ccl_bplist.py: https://github.com/cclgroupltd/ccl-bplist 75 | * mac\_alias: https://pypi.python.org/pypi/mac_alias 76 | -------------------------------------------------------------------------------- /ccl_bplist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2012-2016, CCL Forensics 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the CCL Forensics nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL CCL FORENSICS BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | import sys 29 | import os 30 | import struct 31 | import datetime 32 | 33 | __version__ = "0.21" 34 | __description__ = "Converts Apple binary PList files into a native Python data structure" 35 | __contact__ = "Alex Caithness" 36 | 37 | _object_converter = None 38 | def set_object_converter(function): 39 | """Sets the object converter function to be used when retrieving objects from the bplist. 40 | default is None (which will return objects in their raw form). 41 | A built in converter (ccl_bplist.NSKeyedArchiver_common_objects_convertor) which is geared 42 | toward dealling with common types in NSKeyedArchiver is available which can simplify code greatly 43 | when dealling with these types of files.""" 44 | if not hasattr(function, "__call__"): 45 | raise TypeError("function is not a function") 46 | global _object_converter 47 | _object_converter = function 48 | 49 | class BplistError(Exception): 50 | pass 51 | 52 | class BplistUID: 53 | def __init__(self, value): 54 | self.value = value 55 | 56 | def __repr__(self): 57 | return "UID: {0}".format(self.value) 58 | 59 | def __str__(self): 60 | return self.__repr__() 61 | 62 | def __decode_multibyte_int(b, signed=True): 63 | if len(b) == 1: 64 | fmt = ">B" # Always unsigned? 65 | elif len(b) == 2: 66 | fmt = ">h" 67 | elif len(b) == 3: 68 | if signed: 69 | return ((b[0] << 16) | struct.unpack(">H", b[1:])[0]) - ((b[0] >> 7) * 2 * 0x800000) 70 | else: 71 | return (b[0] << 16) | struct.unpack(">H", b[1:])[0] 72 | elif len(b) == 4: 73 | fmt = ">i" 74 | elif len(b) == 8: 75 | fmt = ">q" 76 | elif len(b) == 16: 77 | # special case for BigIntegers 78 | high, low = struct.unpack(">QQ", b) 79 | result = (high << 64) | low 80 | if high & 0x8000000000000000 and signed: 81 | result -= 0x100000000000000000000000000000000 82 | return result 83 | else: 84 | raise BplistError("Cannot decode multibyte int of length {0}".format(len(b))) 85 | 86 | if signed and len(b) > 1: 87 | return struct.unpack(fmt.lower(), b)[0] 88 | else: 89 | return struct.unpack(fmt.upper(), b)[0] 90 | 91 | def __decode_float(b, signed=True): 92 | if len(b) == 4: 93 | fmt = ">f" 94 | elif len(b) == 8: 95 | fmt = ">d" 96 | else: 97 | raise BplistError("Cannot decode float of length {0}".format(len(b))) 98 | 99 | if signed: 100 | return struct.unpack(fmt.lower(), b)[0] 101 | else: 102 | return struct.unpack(fmt.upper(), b)[0] 103 | 104 | def __decode_object(f, offset, collection_offset_size, offset_table): 105 | # Move to offset and read type 106 | #print("Decoding object at offset {0}".format(offset)) 107 | f.seek(offset) 108 | # A little hack to keep the script portable between py2.x and py3k 109 | if sys.version_info[0] < 3: 110 | type_byte = ord(f.read(1)[0]) 111 | else: 112 | type_byte = f.read(1)[0] 113 | #print("Type byte: {0}".format(hex(type_byte))) 114 | if type_byte == 0x00: # Null 0000 0000 115 | return None 116 | elif type_byte == 0x08: # False 0000 1000 117 | return False 118 | elif type_byte == 0x09: # True 0000 1001 119 | return True 120 | elif type_byte == 0x0F: # Fill 0000 1111 121 | raise BplistError("Fill type not currently supported at offset {0}".format(f.tell())) # Not sure what to return really... 122 | elif type_byte & 0xF0 == 0x10: # Int 0001 xxxx 123 | int_length = 2 ** (type_byte & 0x0F) 124 | int_bytes = f.read(int_length) 125 | return __decode_multibyte_int(int_bytes) 126 | elif type_byte & 0xF0 == 0x20: # Float 0010 nnnn 127 | float_length = 2 ** (type_byte & 0x0F) 128 | float_bytes = f.read(float_length) 129 | return __decode_float(float_bytes) 130 | elif type_byte & 0xFF == 0x33: # Date 0011 0011 131 | date_bytes = f.read(8) 132 | date_value = __decode_float(date_bytes) 133 | try: 134 | result = datetime.datetime(2001,1,1) + datetime.timedelta(seconds = date_value) 135 | except OverflowError: 136 | result = datetime.datetime.min 137 | return result 138 | elif type_byte & 0xF0 == 0x40: # Data 0100 nnnn 139 | if type_byte & 0x0F != 0x0F: 140 | # length in 4 lsb 141 | data_length = type_byte & 0x0F 142 | else: 143 | # A little hack to keep the script portable between py2.x and py3k 144 | if sys.version_info[0] < 3: 145 | int_type_byte = ord(f.read(1)[0]) 146 | else: 147 | int_type_byte = f.read(1)[0] 148 | if int_type_byte & 0xF0 != 0x10: 149 | raise BplistError("Long Data field definition not followed by int type at offset {0}".format(f.tell())) 150 | int_length = 2 ** (int_type_byte & 0x0F) 151 | int_bytes = f.read(int_length) 152 | data_length = __decode_multibyte_int(int_bytes, False) 153 | return f.read(data_length) 154 | elif type_byte & 0xF0 == 0x50: # ASCII 0101 nnnn 155 | if type_byte & 0x0F != 0x0F: 156 | # length in 4 lsb 157 | ascii_length = type_byte & 0x0F 158 | else: 159 | # A little hack to keep the script portable between py2.x and py3k 160 | if sys.version_info[0] < 3: 161 | int_type_byte = ord(f.read(1)[0]) 162 | else: 163 | int_type_byte = f.read(1)[0] 164 | if int_type_byte & 0xF0 != 0x10: 165 | raise BplistError("Long ASCII field definition not followed by int type at offset {0}".format(f.tell())) 166 | int_length = 2 ** (int_type_byte & 0x0F) 167 | int_bytes = f.read(int_length) 168 | ascii_length = __decode_multibyte_int(int_bytes, False) 169 | return f.read(ascii_length).decode("ascii") 170 | elif type_byte & 0xF0 == 0x60: # UTF-16 0110 nnnn 171 | if type_byte & 0x0F != 0x0F: 172 | # length in 4 lsb 173 | utf16_length = (type_byte & 0x0F) * 2 # Length is characters - 16bit width 174 | else: 175 | # A little hack to keep the script portable between py2.x and py3k 176 | if sys.version_info[0] < 3: 177 | int_type_byte = ord(f.read(1)[0]) 178 | else: 179 | int_type_byte = f.read(1)[0] 180 | if int_type_byte & 0xF0 != 0x10: 181 | raise BplistError("Long UTF-16 field definition not followed by int type at offset {0}".format(f.tell())) 182 | int_length = 2 ** (int_type_byte & 0x0F) 183 | int_bytes = f.read(int_length) 184 | utf16_length = __decode_multibyte_int(int_bytes, False) * 2 185 | return f.read(utf16_length).decode("utf_16_be") 186 | elif type_byte & 0xF0 == 0x80: # UID 1000 nnnn 187 | uid_length = (type_byte & 0x0F) + 1 188 | uid_bytes = f.read(uid_length) 189 | return BplistUID(__decode_multibyte_int(uid_bytes, signed=False)) 190 | elif type_byte & 0xF0 == 0xA0: # Array 1010 nnnn 191 | if type_byte & 0x0F != 0x0F: 192 | # length in 4 lsb 193 | array_count = type_byte & 0x0F 194 | else: 195 | # A little hack to keep the script portable between py2.x and py3k 196 | if sys.version_info[0] < 3: 197 | int_type_byte = ord(f.read(1)[0]) 198 | else: 199 | int_type_byte = f.read(1)[0] 200 | if int_type_byte & 0xF0 != 0x10: 201 | raise BplistError("Long Array field definition not followed by int type at offset {0}".format(f.tell())) 202 | int_length = 2 ** (int_type_byte & 0x0F) 203 | int_bytes = f.read(int_length) 204 | array_count = __decode_multibyte_int(int_bytes, signed=False) 205 | array_refs = [] 206 | for i in range(array_count): 207 | array_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False)) 208 | return [__decode_object(f, offset_table[obj_ref], collection_offset_size, offset_table) for obj_ref in array_refs] 209 | elif type_byte & 0xF0 == 0xC0: # Set 1010 nnnn 210 | if type_byte & 0x0F != 0x0F: 211 | # length in 4 lsb 212 | set_count = type_byte & 0x0F 213 | else: 214 | # A little hack to keep the script portable between py2.x and py3k 215 | if sys.version_info[0] < 3: 216 | int_type_byte = ord(f.read(1)[0]) 217 | else: 218 | int_type_byte = f.read(1)[0] 219 | if int_type_byte & 0xF0 != 0x10: 220 | raise BplistError("Long Set field definition not followed by int type at offset {0}".format(f.tell())) 221 | int_length = 2 ** (int_type_byte & 0x0F) 222 | int_bytes = f.read(int_length) 223 | set_count = __decode_multibyte_int(int_bytes, signed=False) 224 | set_refs = [] 225 | for i in range(set_count): 226 | set_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False)) 227 | return [__decode_object(f, offset_table[obj_ref], collection_offset_size, offset_table) for obj_ref in set_refs] 228 | elif type_byte & 0xF0 == 0xD0: # Dict 1011 nnnn 229 | if type_byte & 0x0F != 0x0F: 230 | # length in 4 lsb 231 | dict_count = type_byte & 0x0F 232 | else: 233 | # A little hack to keep the script portable between py2.x and py3k 234 | if sys.version_info[0] < 3: 235 | int_type_byte = ord(f.read(1)[0]) 236 | else: 237 | int_type_byte = f.read(1)[0] 238 | #print("Dictionary length int byte: {0}".format(hex(int_type_byte))) 239 | if int_type_byte & 0xF0 != 0x10: 240 | raise BplistError("Long Dict field definition not followed by int type at offset {0}".format(f.tell())) 241 | int_length = 2 ** (int_type_byte & 0x0F) 242 | int_bytes = f.read(int_length) 243 | dict_count = __decode_multibyte_int(int_bytes, signed=False) 244 | key_refs = [] 245 | #print("Dictionary count: {0}".format(dict_count)) 246 | for i in range(dict_count): 247 | key_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False)) 248 | value_refs = [] 249 | for i in range(dict_count): 250 | value_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False)) 251 | 252 | dict_result = {} 253 | for i in range(dict_count): 254 | #print("Key ref: {0}\tVal ref: {1}".format(key_refs[i], value_refs[i])) 255 | key = __decode_object(f, offset_table[key_refs[i]], collection_offset_size, offset_table) 256 | val = __decode_object(f, offset_table[value_refs[i]], collection_offset_size, offset_table) 257 | dict_result[key] = val 258 | return dict_result 259 | 260 | 261 | def load(f): 262 | """ 263 | Reads and converts a file-like object containing a binary property list. 264 | Takes a file-like object (must support reading and seeking) as an argument 265 | Returns a data structure representing the data in the property list 266 | """ 267 | # Check magic number 268 | if f.read(8) != b"bplist00": 269 | raise BplistError("Bad file header") 270 | 271 | # Read trailer 272 | f.seek(-32, os.SEEK_END) 273 | trailer = f.read(32) 274 | offset_int_size, collection_offset_size, object_count, top_level_object_index, offest_table_offset = struct.unpack(">6xbbQQQ", trailer) 275 | 276 | # Read offset table 277 | f.seek(offest_table_offset) 278 | offset_table = [] 279 | for i in range(object_count): 280 | offset_table.append(__decode_multibyte_int(f.read(offset_int_size), False)) 281 | 282 | return __decode_object(f, offset_table[top_level_object_index], collection_offset_size, offset_table) 283 | 284 | 285 | def NSKeyedArchiver_common_objects_convertor(o): 286 | """Built in converter function (suitable for submission to set_object_converter()) which automatically 287 | converts the following common data-types found in NSKeyedArchiver: 288 | NSDictionary/NSMutableDictionary; 289 | NSArray/NSMutableArray; 290 | NSSet/NSMutableSet 291 | NSString/NSMutableString 292 | NSDate 293 | $null strings""" 294 | # Conversion: NSDictionary 295 | if is_nsmutabledictionary(o): 296 | return convert_NSMutableDictionary(o) 297 | # Conversion: NSArray 298 | elif is_nsarray(o): 299 | return convert_NSArray(o) 300 | elif is_isnsset(o): 301 | return convert_NSSet(o) 302 | # Conversion: NSString 303 | elif is_nsstring(o): 304 | return convert_NSString(o) 305 | # Conversion: NSDate 306 | elif is_nsdate(o): 307 | return convert_NSDate(o) 308 | # Conversion: "$null" string 309 | elif isinstance(o, str) and o == "$null": 310 | return None 311 | # Fallback: 312 | else: 313 | return o 314 | 315 | def NSKeyedArchiver_convert(o, object_table): 316 | if isinstance(o, list): 317 | #return NsKeyedArchiverList(o, object_table) 318 | result = NsKeyedArchiverList(o, object_table) 319 | elif isinstance(o, dict): 320 | #return NsKeyedArchiverDictionary(o, object_table) 321 | result = NsKeyedArchiverDictionary(o, object_table) 322 | elif isinstance(o, BplistUID): 323 | #return NSKeyedArchiver_convert(object_table[o.value], object_table) 324 | result = NSKeyedArchiver_convert(object_table[o.value], object_table) 325 | else: 326 | #return o 327 | result = o 328 | 329 | if _object_converter: 330 | return _object_converter(result) 331 | else: 332 | return result 333 | 334 | 335 | class NsKeyedArchiverDictionary(dict): 336 | def __init__(self, original_dict, object_table): 337 | super(NsKeyedArchiverDictionary, self).__init__(original_dict) 338 | self.object_table = object_table 339 | 340 | def __getitem__(self, index): 341 | o = super(NsKeyedArchiverDictionary, self).__getitem__(index) 342 | return NSKeyedArchiver_convert(o, self.object_table) 343 | 344 | def get(self, key, default=None): 345 | return self[key] if key in self else default 346 | 347 | def values(self): 348 | for k in self: 349 | yield self[k] 350 | 351 | def items(self): 352 | for k in self: 353 | yield k, self[k] 354 | 355 | class NsKeyedArchiverList(list): 356 | def __init__(self, original_iterable, object_table): 357 | super(NsKeyedArchiverList, self).__init__(original_iterable) 358 | self.object_table = object_table 359 | 360 | def __getitem__(self, index): 361 | o = super(NsKeyedArchiverList, self).__getitem__(index) 362 | return NSKeyedArchiver_convert(o, self.object_table) 363 | 364 | def __iter__(self): 365 | for o in super(NsKeyedArchiverList, self).__iter__(): 366 | yield NSKeyedArchiver_convert(o, self.object_table) 367 | 368 | 369 | def deserialise_NsKeyedArchiver(obj, parse_whole_structure=False): 370 | """Deserialises an NSKeyedArchiver bplist rebuilding the structure. 371 | obj should usually be the top-level object returned by the load() 372 | function.""" 373 | 374 | # Check that this is an archiver and version we understand 375 | if not isinstance(obj, dict): 376 | raise TypeError("obj must be a dict") 377 | if "$archiver" not in obj or obj["$archiver"] not in ("NSKeyedArchiver", "NRKeyedArchiver"): 378 | raise ValueError("obj does not contain an '$archiver' key or the '$archiver' is unrecognised") 379 | if "$version" not in obj or obj["$version"] != 100000: 380 | raise ValueError("obj does not contain a '$version' key or the '$version' is unrecognised") 381 | 382 | object_table = obj["$objects"] 383 | if "root" in obj["$top"] and not parse_whole_structure: 384 | return NSKeyedArchiver_convert(obj["$top"]["root"], object_table) 385 | else: 386 | return NSKeyedArchiver_convert(obj["$top"], object_table) 387 | 388 | # NSMutableDictionary convenience functions 389 | def is_nsmutabledictionary(obj): 390 | if not isinstance(obj, dict): 391 | return False 392 | if "$class" not in obj.keys(): 393 | return False 394 | if obj["$class"].get("$classname") not in ("NSMutableDictionary", "NSDictionary"): 395 | return False 396 | if "NS.keys" not in obj.keys(): 397 | return False 398 | if "NS.objects" not in obj.keys(): 399 | return False 400 | 401 | return True 402 | 403 | def convert_NSMutableDictionary(obj): 404 | """Converts a NSKeyedArchiver serialised NSMutableDictionary into 405 | a straight dictionary (rather than two lists as it is serialised 406 | as)""" 407 | 408 | # The dictionary is serialised as two lists (one for keys and one 409 | # for values) which obviously removes all convenience afforded by 410 | # dictionaries. This function converts this structure to an 411 | # actual dictionary so that values can be accessed by key. 412 | 413 | if not is_nsmutabledictionary(obj): 414 | raise ValueError("obj does not have the correct structure for a NSDictionary/NSMutableDictionary serialised to a NSKeyedArchiver") 415 | keys = obj["NS.keys"] 416 | vals = obj["NS.objects"] 417 | 418 | # sense check the keys and values: 419 | if not isinstance(keys, list): 420 | raise TypeError("The 'NS.keys' value is an unexpected type (expected list; actual: {0}".format(type(keys))) 421 | if not isinstance(vals, list): 422 | raise TypeError("The 'NS.objects' value is an unexpected type (expected list; actual: {0}".format(type(vals))) 423 | if len(keys) != len(vals): 424 | raise ValueError("The length of the 'NS.keys' list ({0}) is not equal to that of the 'NS.objects ({1})".format(len(keys), len(vals))) 425 | 426 | result = {} 427 | for i,k in enumerate(keys): 428 | if k in result: 429 | raise ValueError("The 'NS.keys' list contains duplicate entries") 430 | result[k] = vals[i] 431 | 432 | return result 433 | 434 | # NSArray convenience functions 435 | def is_nsarray(obj): 436 | if not isinstance(obj, dict): 437 | return False 438 | if "$class" not in obj.keys(): 439 | return False 440 | if obj["$class"].get("$classname") not in ("NSArray", "NSMutableArray"): 441 | return False 442 | if "NS.objects" not in obj.keys(): 443 | return False 444 | 445 | return True 446 | 447 | def convert_NSArray(obj): 448 | if not is_nsarray(obj): 449 | raise ValueError("obj does not have the correct structure for a NSArray/NSMutableArray serialised to a NSKeyedArchiver") 450 | 451 | return obj["NS.objects"] 452 | 453 | # NSSet convenience functions 454 | def is_isnsset(obj): 455 | if not isinstance(obj, dict): 456 | return False 457 | if "$class" not in obj.keys(): 458 | return False 459 | if obj["$class"].get("$classname") not in ("NSSet", "NSMutableSet"): 460 | return False 461 | if "NS.objects" not in obj.keys(): 462 | return False 463 | 464 | return True 465 | 466 | def convert_NSSet(obj): 467 | if not is_isnsset(obj): 468 | raise ValueError("obj does not have the correct structure for a NSSet/NSMutableSet serialised to a NSKeyedArchiver") 469 | 470 | return list(obj["NS.objects"]) 471 | 472 | # NSString convenience functions 473 | def is_nsstring(obj): 474 | if not isinstance(obj, dict): 475 | return False 476 | if "$class" not in obj.keys(): 477 | return False 478 | if obj["$class"].get("$classname") not in ("NSString", "NSMutableString"): 479 | return False 480 | if "NS.string" not in obj.keys(): 481 | return False 482 | return True 483 | 484 | def convert_NSString(obj): 485 | if not is_nsstring(obj): 486 | raise ValueError("obj does not have the correct structure for a NSString/NSMutableString serialised to a NSKeyedArchiver") 487 | 488 | return obj["NS.string"] 489 | 490 | # NSDate convenience functions 491 | def is_nsdate(obj): 492 | if not isinstance(obj, dict): 493 | return False 494 | if "$class" not in obj.keys(): 495 | return False 496 | if obj["$class"].get("$classname") not in ("NSDate"): 497 | return False 498 | if "NS.time" not in obj.keys(): 499 | return False 500 | 501 | return True 502 | 503 | def convert_NSDate(obj): 504 | if not is_nsdate(obj): 505 | raise ValueError("obj does not have the correct structure for a NSDate serialised to a NSKeyedArchiver") 506 | 507 | return datetime.datetime(2001, 1, 1) + datetime.timedelta(seconds=obj["NS.time"]) 508 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpco/AlfredWorkflow-Recent-Documents/75fad35b380baaf048fedda569eb9b7bf754a5b1/icon.png -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | com.mpco.recentDocuments 7 | category 8 | Tools 9 | connections 10 | 11 | 242EA50D-B03A-4278-A412-B4AFC16AC51F 12 | 13 | 14 | destinationuid 15 | FEEBCB26-E74C-411D-9DC4-B8E92B0A9DA8 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | 30D86D1A-D4A8-4B08-8574-CBF2835E4A96 25 | 26 | 27 | destinationuid 28 | D18D2ED7-42CA-45C3-B69D-C9759BD2679B 29 | modifiers 30 | 0 31 | modifiersubtext 32 | 33 | vitoclose 34 | 35 | 36 | 37 | 52A9E327-10AF-4FA5-B6D8-8FA26DC6C19C 38 | 39 | 40 | destinationuid 41 | 30D86D1A-D4A8-4B08-8574-CBF2835E4A96 42 | modifiers 43 | 0 44 | modifiersubtext 45 | 46 | vitoclose 47 | 48 | 49 | 50 | 53BF5810-7308-4550-A1C3-92DC01964658 51 | 52 | 53 | destinationuid 54 | B6340DCC-2D0D-4367-BAC9-C1D922545CB0 55 | modifiers 56 | 0 57 | modifiersubtext 58 | 59 | vitoclose 60 | 61 | 62 | 63 | 5459E1D5-34AA-4E81-AAC7-8D52C4077AE9 64 | 65 | 66 | destinationuid 67 | D172CEA8-0DE7-4AA2-BBDA-7819B2252316 68 | modifiers 69 | 0 70 | modifiersubtext 71 | 72 | vitoclose 73 | 74 | 75 | 76 | 79D60C12-3480-4744-93FD-5B6AA769F31A 77 | 78 | 79 | destinationuid 80 | B4CFDC4F-AF3F-47C2-8100-476F7E6FF72E 81 | modifiers 82 | 0 83 | modifiersubtext 84 | 85 | vitoclose 86 | 87 | 88 | 89 | A46BBC3D-A323-49F8-90F1-A58975C5AF06 90 | 91 | 92 | destinationuid 93 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595 94 | modifiers 95 | 0 96 | modifiersubtext 97 | 98 | vitoclose 99 | 100 | 101 | 102 | B4CFDC4F-AF3F-47C2-8100-476F7E6FF72E 103 | 104 | 105 | destinationuid 106 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595 107 | modifiers 108 | 0 109 | modifiersubtext 110 | 111 | vitoclose 112 | 113 | 114 | 115 | B6340DCC-2D0D-4367-BAC9-C1D922545CB0 116 | 117 | 118 | destinationuid 119 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595 120 | modifiers 121 | 0 122 | modifiersubtext 123 | 124 | vitoclose 125 | 126 | 127 | 128 | D172CEA8-0DE7-4AA2-BBDA-7819B2252316 129 | 130 | 131 | destinationuid 132 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595 133 | modifiers 134 | 0 135 | modifiersubtext 136 | 137 | vitoclose 138 | 139 | 140 | 141 | D18D2ED7-42CA-45C3-B69D-C9759BD2679B 142 | 143 | 144 | destinationuid 145 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595 146 | modifiers 147 | 0 148 | modifiersubtext 149 | 150 | vitoclose 151 | 152 | 153 | 154 | FEEBCB26-E74C-411D-9DC4-B8E92B0A9DA8 155 | 156 | 157 | destinationuid 158 | A46BBC3D-A323-49F8-90F1-A58975C5AF06 159 | modifiers 160 | 0 161 | modifiersubtext 162 | 163 | vitoclose 164 | 165 | 166 | 167 | 168 | createdby 169 | Charles Ma 170 | description 171 | Open recent documents or apps 172 | disabled 173 | 174 | name 175 | Recent Documents / Apps 176 | objects 177 | 178 | 179 | config 180 | 181 | action 182 | 0 183 | argument 184 | 0 185 | focusedappvariable 186 | 187 | focusedappvariablename 188 | 189 | hotkey 190 | 0 191 | hotmod 192 | 0 193 | leftcursor 194 | 195 | modsmode 196 | 0 197 | relatedAppsMode 198 | 0 199 | 200 | type 201 | alfred.workflow.trigger.hotkey 202 | uid 203 | 79D60C12-3480-4744-93FD-5B6AA769F31A 204 | version 205 | 2 206 | 207 | 208 | config 209 | 210 | alfredfiltersresults 211 | 212 | alfredfiltersresultsmatchmode 213 | 0 214 | argumenttreatemptyqueryasnil 215 | 216 | argumenttrimmode 217 | 0 218 | argumenttype 219 | 1 220 | escaping 221 | 102 222 | keyword 223 | ra 224 | queuedelaycustom 225 | 3 226 | queuedelayimmediatelyinitially 227 | 228 | queuedelaymode 229 | 0 230 | queuemode 231 | 1 232 | runningsubtext 233 | 234 | script 235 | # python -O xxx.py, run 236 | # python xxx.py, debug 237 | 238 | macOSVersion=$(sw_vers -productVersion | cut -d '.' -f 1,2) 239 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } 240 | 241 | if [ $(version $macOSVersion) -le $(version "10.12") ]; then 242 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentApplications.sfl" 243 | elif [ $(version $macOSVersion) -lt $(version "12.3") ]; then 244 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentApplications.sfl2" 245 | elif [ $(version $macOSVersion) -lt $(version "14.0") ]; then 246 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentApplications.sfl2" 247 | else 248 | /usr/bin/python3 -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentApplications.sfl3" 249 | fi 250 | scriptargtype 251 | 1 252 | scriptfile 253 | 254 | subtext 255 | Tap "ra" to show 256 | title 257 | Recent apps 258 | type 259 | 0 260 | withspace 261 | 262 | 263 | type 264 | alfred.workflow.input.scriptfilter 265 | uid 266 | B4CFDC4F-AF3F-47C2-8100-476F7E6FF72E 267 | version 268 | 3 269 | 270 | 271 | config 272 | 273 | openwith 274 | 275 | sourcefile 276 | 277 | 278 | type 279 | alfred.workflow.action.openfile 280 | uid 281 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595 282 | version 283 | 3 284 | 285 | 286 | config 287 | 288 | action 289 | 0 290 | argument 291 | 0 292 | focusedappvariable 293 | 294 | focusedappvariablename 295 | 296 | hotkey 297 | 0 298 | hotmod 299 | 0 300 | leftcursor 301 | 302 | modsmode 303 | 0 304 | relatedAppsMode 305 | 0 306 | 307 | type 308 | alfred.workflow.trigger.hotkey 309 | uid 310 | 5459E1D5-34AA-4E81-AAC7-8D52C4077AE9 311 | version 312 | 2 313 | 314 | 315 | config 316 | 317 | alfredfiltersresults 318 | 319 | alfredfiltersresultsmatchmode 320 | 0 321 | argumenttreatemptyqueryasnil 322 | 323 | argumenttrimmode 324 | 0 325 | argumenttype 326 | 1 327 | escaping 328 | 102 329 | keyword 330 | rd 331 | queuedelaycustom 332 | 3 333 | queuedelayimmediatelyinitially 334 | 335 | queuedelaymode 336 | 0 337 | queuemode 338 | 1 339 | runningsubtext 340 | 341 | script 342 | macOSVersion=$(sw_vers -productVersion | cut -d '.' -f 1,2) 343 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } 344 | 345 | if [ $(version $macOSVersion) -le $(version "10.12") ]; then 346 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentDocuments.sfl" 347 | elif [ $(version $macOSVersion) -lt $(version "12.3") ]; then 348 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentDocuments.sfl2" 349 | elif [ $(version $macOSVersion) -lt $(version "14.0") ]; then 350 | /usr/bin/python -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentDocuments.sfl2" 351 | else 352 | /usr/bin/python3 -O main.py "${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentDocuments.sfl3" 353 | fi 354 | scriptargtype 355 | 1 356 | scriptfile 357 | 358 | subtext 359 | Tap "rd" to show 360 | title 361 | Recent documents 362 | type 363 | 0 364 | withspace 365 | 366 | 367 | type 368 | alfred.workflow.input.scriptfilter 369 | uid 370 | D172CEA8-0DE7-4AA2-BBDA-7819B2252316 371 | version 372 | 3 373 | 374 | 375 | config 376 | 377 | action 378 | 0 379 | argument 380 | 0 381 | focusedappvariable 382 | 383 | focusedappvariablename 384 | 385 | hotkey 386 | 0 387 | hotmod 388 | 0 389 | leftcursor 390 | 391 | modsmode 392 | 0 393 | relatedAppsMode 394 | 0 395 | 396 | type 397 | alfred.workflow.trigger.hotkey 398 | uid 399 | 53BF5810-7308-4550-A1C3-92DC01964658 400 | version 401 | 2 402 | 403 | 404 | config 405 | 406 | alfredfiltersresults 407 | 408 | alfredfiltersresultsmatchmode 409 | 0 410 | argumenttreatemptyqueryasnil 411 | 412 | argumenttrimmode 413 | 0 414 | argumenttype 415 | 1 416 | escaping 417 | 102 418 | keyword 419 | rf 420 | queuedelaycustom 421 | 3 422 | queuedelayimmediatelyinitially 423 | 424 | queuedelaymode 425 | 0 426 | queuemode 427 | 1 428 | runningsubtext 429 | 430 | script 431 | macOSVersion=$(sw_vers -productVersion | cut -d '.' -f 1,2) 432 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } 433 | 434 | if [ $(version $macOSVersion) -lt $(version "12.3") ]; then 435 | /usr/bin/python -O main.py "${HOME}/Library/Preferences/com.apple.finder.plist" 436 | else 437 | /usr/bin/python3 -O main.py "${HOME}/Library/Preferences/com.apple.finder.plist" 438 | fi 439 | 440 | scriptargtype 441 | 1 442 | scriptfile 443 | 444 | subtext 445 | Tap "rff" to show 446 | title 447 | Recent folder 448 | type 449 | 0 450 | withspace 451 | 452 | 453 | type 454 | alfred.workflow.input.scriptfilter 455 | uid 456 | B6340DCC-2D0D-4367-BAC9-C1D922545CB0 457 | version 458 | 3 459 | 460 | 461 | config 462 | 463 | json 464 | { 465 | "alfredworkflow" : { 466 | "arg" : "{query}", 467 | "config" : { 468 | "openwith" : "{var:currentAppPath}", 469 | "sourcefile" : "" 470 | }, 471 | "variables" : { 472 | } 473 | } 474 | } 475 | 476 | type 477 | alfred.workflow.utility.json 478 | uid 479 | A46BBC3D-A323-49F8-90F1-A58975C5AF06 480 | version 481 | 1 482 | 483 | 484 | config 485 | 486 | action 487 | 0 488 | argument 489 | 0 490 | focusedappvariable 491 | 492 | focusedappvariablename 493 | 494 | hotkey 495 | 0 496 | hotmod 497 | 0 498 | leftcursor 499 | 500 | modsmode 501 | 0 502 | relatedAppsMode 503 | 0 504 | 505 | type 506 | alfred.workflow.trigger.hotkey 507 | uid 508 | 242EA50D-B03A-4278-A412-B4AFC16AC51F 509 | version 510 | 2 511 | 512 | 513 | config 514 | 515 | alfredfiltersresults 516 | 517 | alfredfiltersresultsmatchmode 518 | 0 519 | argumenttreatemptyqueryasnil 520 | 521 | argumenttrimmode 522 | 0 523 | argumenttype 524 | 1 525 | escaping 526 | 102 527 | keyword 528 | rr 529 | queuedelaycustom 530 | 3 531 | queuedelayimmediatelyinitially 532 | 533 | queuedelaymode 534 | 0 535 | queuemode 536 | 1 537 | runningsubtext 538 | 539 | script 540 | 541 | # check macos version 542 | macOSVersion=$(sw_vers -productVersion | cut -d '.' -f 1,2) 543 | 544 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } 545 | 546 | if [ $(version $macOSVersion) -le $(version "10.12") ]; then 547 | MRUfileExt="sfl" 548 | elif [ $(version $macOSVersion) -lt $(version "14.00") ]; then 549 | MRUfileExt="sfl2" 550 | else 551 | MRUfileExt="sfl3" 552 | fi 553 | 554 | # check app 555 | currentAppID=$(osascript -e "tell application (path to frontmost application as text) to get id") 556 | 557 | export currentAppPath=$(osascript -e 'tell application "Finder" to get text 1 thru -2 of POSIX path of (path to frontmost application)') 558 | if [ "${currentAppID}" = "com.apple.finder" ]; then 559 | MRUfile="${HOME}/Library/Preferences/com.apple.finder.plist" 560 | elif [ "${currentAppID}" = "com.microsoft.Word" ] || [ "${currentAppID}" = "com.microsoft.Powerpoint" ] || [ "${currentAppID}" = "com.microsoft.Excel" ] ; then 561 | MRUfile="${HOME}/Library/Containers/${currentAppID}/Data/Library/Preferences/${currentAppID}.securebookmarks.plist" 562 | elif [ "${currentAppID}" = "com.sublimetext.3" ]; then 563 | AutoSaveSessionFile="${HOME}/Library/Application Support/Sublime Text 3/Local/Auto Save Session.sublime_session" 564 | if [ -f "$AutoSaveSessionFile" ]; then 565 | MRUfile="$AutoSaveSessionFile" 566 | else 567 | MRUfile="${HOME}/Library/Application Support/Sublime Text 3/Local/Session.sublime_session" 568 | fi 569 | else 570 | MRUfile="${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/${currentAppID}.${MRUfileExt}" 571 | fi 572 | 573 | 574 | if [ -f "${MRUfile}" ]; then 575 | if [ $(version $macOSVersion) -lt $(version "12.3") ]; then 576 | /usr/bin/python -O main.py "${MRUfile}" 577 | else 578 | /usr/bin/python3 -O main.py "${MRUfile}" 579 | fi 580 | else 581 | echo '{"items": [{"title": "None Recent Record for the foremost app","subtitle": "(*´・д・)?"}]}' 582 | fi 583 | scriptargtype 584 | 1 585 | scriptfile 586 | 587 | subtext 588 | Tap "rf" to show 589 | title 590 | Recent documents for the foremost app 591 | type 592 | 0 593 | withspace 594 | 595 | 596 | type 597 | alfred.workflow.input.scriptfilter 598 | uid 599 | FEEBCB26-E74C-411D-9DC4-B8E92B0A9DA8 600 | version 601 | 3 602 | 603 | 604 | config 605 | 606 | json 607 | { 608 | "alfredworkflow" : { 609 | "arg" : "{query}", 610 | "config" : { 611 | "openwith" : "{var:currentAppPath}", 612 | "sourcefile" : "" 613 | }, 614 | "variables" : { 615 | } 616 | } 617 | } 618 | 619 | type 620 | alfred.workflow.utility.json 621 | uid 622 | D18D2ED7-42CA-45C3-B69D-C9759BD2679B 623 | version 624 | 1 625 | 626 | 627 | config 628 | 629 | alfredfiltersresults 630 | 631 | alfredfiltersresultsmatchmode 632 | 0 633 | argumenttreatemptyqueryasnil 634 | 635 | argumenttrimmode 636 | 0 637 | argumenttype 638 | 1 639 | escaping 640 | 102 641 | keyword 642 | code 643 | queuedelaycustom 644 | 3 645 | queuedelayimmediatelyinitially 646 | 647 | queuedelaymode 648 | 0 649 | queuemode 650 | 1 651 | runningsubtext 652 | 653 | script 654 | 655 | # check macos version 656 | macOSVersion=$(sw_vers -productVersion | cut -d '.' -f 1,2) 657 | 658 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } 659 | 660 | if [ $(version $macOSVersion) -le $(version "10.12") ]; then 661 | MRUfileExt="sfl" 662 | elif [ $(version $macOSVersion) -lt $(version "14.00") ]; then 663 | MRUfileExt="sfl2" 664 | else 665 | MRUfileExt="sfl3" 666 | fi 667 | 668 | # check app 669 | # currentAppID=$(osascript -e "tell application (path to frontmost application as text) to get id") 670 | currentAppID="com.microsoft.VSCode" 671 | # export currentAppPath=$(osascript -e 'tell application "Finder" to get text 1 thru -2 of POSIX path of (path to frontmost application)') 672 | export currentAppPath="/Applications/Visual Studio Code.app" 673 | if [ "${currentAppID}" = "com.apple.finder" ]; then 674 | MRUfile="${HOME}/Library/Preferences/com.apple.finder.plist" 675 | elif [ "${currentAppID}" = "com.microsoft.Word" ] || [ "${currentAppID}" = "com.microsoft.Powerpoint" ] || [ "${currentAppID}" = "com.microsoft.Excel" ] ; then 676 | MRUfile="${HOME}/Library/Containers/${currentAppID}/Data/Library/Preferences/${currentAppID}.securebookmarks.plist" 677 | elif [ "${currentAppID}" = "com.sublimetext.3" ]; then 678 | AutoSaveSessionFile="${HOME}/Library/Application Support/Sublime Text 3/Local/Auto Save Session.sublime_session" 679 | if [ -f "$AutoSaveSessionFile" ]; then 680 | MRUfile="$AutoSaveSessionFile" 681 | else 682 | MRUfile="${HOME}/Library/Application Support/Sublime Text 3/Local/Session.sublime_session" 683 | fi 684 | else 685 | MRUfile="${HOME}/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/${currentAppID}.${MRUfileExt}" 686 | fi 687 | 688 | 689 | if [ -f "${MRUfile}" ]; then 690 | if [ $(version $macOSVersion) -lt $(version "12.3") ]; then 691 | /usr/bin/python -O main.py "${MRUfile}" 692 | else 693 | /usr/bin/python3 -O main.py "${MRUfile}" 694 | fi 695 | else 696 | echo '{"items": [{"title": "None Recent Record for the foremost app","subtitle": "(*´・д・)?"}]}' 697 | fi 698 | scriptargtype 699 | 1 700 | scriptfile 701 | 702 | subtext 703 | Tap "code" to show 704 | title 705 | Recent documents for the vscode 706 | type 707 | 0 708 | withspace 709 | 710 | 711 | type 712 | alfred.workflow.input.scriptfilter 713 | uid 714 | 30D86D1A-D4A8-4B08-8574-CBF2835E4A96 715 | version 716 | 3 717 | 718 | 719 | config 720 | 721 | action 722 | 0 723 | argument 724 | 0 725 | focusedappvariable 726 | 727 | focusedappvariablename 728 | 729 | hotkey 730 | 0 731 | hotmod 732 | 0 733 | leftcursor 734 | 735 | modsmode 736 | 0 737 | relatedAppsMode 738 | 0 739 | 740 | type 741 | alfred.workflow.trigger.hotkey 742 | uid 743 | 52A9E327-10AF-4FA5-B6D8-8FA26DC6C19C 744 | version 745 | 2 746 | 747 | 748 | readme 749 | Website: https://github.com/mpco/AlfredWorkflow-Recent-Documents 750 | uidata 751 | 752 | 242EA50D-B03A-4278-A412-B4AFC16AC51F 753 | 754 | xpos 755 | 90 756 | ypos 757 | 515 758 | 759 | 30D86D1A-D4A8-4B08-8574-CBF2835E4A96 760 | 761 | note 762 | Foremost App's Recent Documents 763 | xpos 764 | 255 765 | ypos 766 | 690 767 | 768 | 52A9E327-10AF-4FA5-B6D8-8FA26DC6C19C 769 | 770 | xpos 771 | 80 772 | ypos 773 | 690 774 | 775 | 53BF5810-7308-4550-A1C3-92DC01964658 776 | 777 | xpos 778 | 100 779 | ypos 780 | 365 781 | 782 | 5459E1D5-34AA-4E81-AAC7-8D52C4077AE9 783 | 784 | xpos 785 | 100 786 | ypos 787 | 205 788 | 789 | 79D60C12-3480-4744-93FD-5B6AA769F31A 790 | 791 | xpos 792 | 100 793 | ypos 794 | 50 795 | 796 | 9F8BAD89-AA36-49E3-AC08-2F634A88C595 797 | 798 | xpos 799 | 700 800 | ypos 801 | 50 802 | 803 | A46BBC3D-A323-49F8-90F1-A58975C5AF06 804 | 805 | xpos 806 | 475 807 | ypos 808 | 420 809 | 810 | B4CFDC4F-AF3F-47C2-8100-476F7E6FF72E 811 | 812 | note 813 | Recent Apps 814 | xpos 815 | 270 816 | ypos 817 | 50 818 | 819 | B6340DCC-2D0D-4367-BAC9-C1D922545CB0 820 | 821 | note 822 | Recent Folders 823 | xpos 824 | 270 825 | ypos 826 | 365 827 | 828 | D172CEA8-0DE7-4AA2-BBDA-7819B2252316 829 | 830 | note 831 | Recent Documents 832 | xpos 833 | 270 834 | ypos 835 | 205 836 | 837 | D18D2ED7-42CA-45C3-B69D-C9759BD2679B 838 | 839 | xpos 840 | 465 841 | ypos 842 | 595 843 | 844 | FEEBCB26-E74C-411D-9DC4-B8E92B0A9DA8 845 | 846 | note 847 | Foremost App's Recent Documents 848 | xpos 849 | 265 850 | ypos 851 | 515 852 | 853 | 854 | userconfigurationconfig 855 | 856 | variables 857 | 858 | ExcludedFiles 859 | 860 | ExcludedFolders 861 | 862 | 863 | variablesdontexport 864 | 865 | ExcludedFiles 866 | ExcludedFolders 867 | 868 | version 869 | 2.20 870 | webaddress 871 | https://github.com/mpco/AlfredWorkflow-Recent-Documents 872 | 873 | 874 | -------------------------------------------------------------------------------- /mac_alias/__init__.py: -------------------------------------------------------------------------------- 1 | from .alias import * 2 | from .bookmark import * 3 | 4 | __all__ = [ 'ALIAS_KIND_FILE', 'ALIAS_KIND_FOLDER', 5 | 'ALIAS_HFS_VOLUME_SIGNATURE', 6 | 'ALIAS_FIXED_DISK', 'ALIAS_NETWORK_DISK', 'ALIAS_400KB_FLOPPY_DISK', 7 | 'ALIAS_800KB_FLOPPY_DISK', 'ALIAS_1_44MB_FLOPPY_DISK', 8 | 'ALIAS_EJECTABLE_DISK', 9 | 'ALIAS_NO_CNID', 10 | 'kBookmarkPath', 'kBookmarkCNIDPath', 'kBookmarkFileProperties', 11 | 'kBookmarkFileName', 'kBookmarkFileID', 'kBookmarkFileCreationDate', 12 | 'kBookmarkTOCPath', 'kBookmarkVolumePath', 13 | 'kBookmarkVolumeURL', 'kBookmarkVolumeName', 'kBookmarkVolumeUUID', 14 | 'kBookmarkVolumeSize', 'kBookmarkVolumeCreationDate', 15 | 'kBookmarkVolumeProperties', 'kBookmarkContainingFolder', 16 | 'kBookmarkUserName', 'kBookmarkUID', 'kBookmarkWasFileReference', 17 | 'kBookmarkCreationOptions', 'kBookmarkURLLengths', 18 | 'kBookmarkSecurityExtension', 19 | 'AppleShareInfo', 20 | 'VolumeInfo', 21 | 'TargetInfo', 22 | 'Alias', 23 | 'Bookmark', 24 | 'Data', 25 | 'URL' ] 26 | 27 | 28 | -------------------------------------------------------------------------------- /mac_alias/alias.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from __future__ import division 4 | 5 | import struct 6 | import datetime 7 | import io 8 | import re 9 | import os 10 | import os.path 11 | import stat 12 | import sys 13 | from unicodedata import normalize 14 | 15 | if sys.platform == 'darwin': 16 | from . import osx 17 | 18 | try: 19 | long 20 | except NameError: 21 | long = int 22 | 23 | from .utils import * 24 | 25 | ALIAS_KIND_FILE = 0 26 | ALIAS_KIND_FOLDER = 1 27 | 28 | ALIAS_HFS_VOLUME_SIGNATURE = b'H+' 29 | 30 | ALIAS_FILESYSTEM_UDF = 'UDF (CD/DVD)' 31 | ALIAS_FILESYSTEM_FAT32 = 'FAT32' 32 | ALIAS_FILESYSTEM_EXFAT = 'exFAT' 33 | ALIAS_FILESYSTEM_HFSX = 'HFSX' 34 | ALIAS_FILESYSTEM_HFSPLUS = 'HFS+' 35 | ALIAS_FILESYSTEM_FTP = 'FTP' 36 | ALIAS_FILESYSTEM_NTFS = 'NTFS' 37 | ALIAS_FILESYSTEM_UNKNOWN = 'unknown' 38 | 39 | ALIAS_FIXED_DISK = 0 40 | ALIAS_NETWORK_DISK = 1 41 | ALIAS_400KB_FLOPPY_DISK = 2 42 | ALIAS_800KB_FLOPPY_DISK = 3 43 | ALIAS_1_44MB_FLOPPY_DISK = 4 44 | ALIAS_EJECTABLE_DISK = 5 45 | 46 | ALIAS_NO_CNID = 0xffffffff 47 | 48 | ALIAS_FSTYPE_MAP = { 49 | # Version 2 aliases 50 | b'HX': ALIAS_FILESYSTEM_HFSX, 51 | b'H+': ALIAS_FILESYSTEM_HFSPLUS, 52 | 53 | # Version 3 aliases 54 | b'BDcu': ALIAS_FILESYSTEM_UDF, 55 | b'BDIS': ALIAS_FILESYSTEM_FAT32, 56 | b'BDxF': ALIAS_FILESYSTEM_EXFAT, 57 | b'HX\0\0': ALIAS_FILESYSTEM_HFSX, 58 | b'H+\0\0': ALIAS_FILESYSTEM_HFSPLUS, 59 | b'KG\0\0': ALIAS_FILESYSTEM_FTP, 60 | b'NTcu': ALIAS_FILESYSTEM_NTFS, 61 | } 62 | 63 | def encode_utf8(s): 64 | if isinstance(s, bytes): 65 | return s 66 | return s.encode('utf-8') 67 | 68 | def decode_utf8(s): 69 | if isinstance(s, bytes): 70 | return s.decode('utf-8') 71 | return s 72 | 73 | class AppleShareInfo (object): 74 | def __init__(self, zone=None, server=None, user=None): 75 | #: The AppleShare zone 76 | self.zone = zone 77 | #: The AFP server 78 | self.server = server 79 | #: The username 80 | self.user = user 81 | 82 | def __repr__(self): 83 | return 'AppleShareInfo(%r,%r,%r)' % (self.zone, self.server, self.user) 84 | 85 | class VolumeInfo (object): 86 | def __init__(self, name, creation_date, fs_type, disk_type, 87 | attribute_flags, fs_id, appleshare_info=None, 88 | driver_name=None, posix_path=None, disk_image_alias=None, 89 | dialup_info=None, network_mount_info=None): 90 | #: The name of the volume on which the target resides 91 | self.name = name 92 | 93 | #: The creation date of the target's volume 94 | self.creation_date = creation_date 95 | 96 | #: The filesystem type 97 | #: (for v2 aliases, this is a 2-character code; for v3 aliases, a 98 | #: 4-character code). 99 | self.fs_type = fs_type 100 | 101 | #: The type of disk; should be one of 102 | #: 103 | #: * ALIAS_FIXED_DISK 104 | #: * ALIAS_NETWORK_DISK 105 | #: * ALIAS_400KB_FLOPPY_DISK 106 | #: * ALIAS_800KB_FLOPPY_DISK 107 | #: * ALIAS_1_44MB_FLOPPY_DISK 108 | #: * ALIAS_EJECTABLE_DISK 109 | self.disk_type = disk_type 110 | 111 | #: Filesystem attribute flags (from HFS volume header) 112 | self.attribute_flags = attribute_flags 113 | 114 | #: Filesystem identifier 115 | self.fs_id = fs_id 116 | 117 | #: AppleShare information (for automatic remounting of network shares) 118 | #: *(optional)* 119 | self.appleshare_info = appleshare_info 120 | 121 | #: Driver name (*probably* contains a disk driver name on older Macs) 122 | #: *(optional)* 123 | self.driver_name = driver_name 124 | 125 | #: POSIX path of the mount point of the target's volume 126 | #: *(optional)* 127 | self.posix_path = posix_path 128 | 129 | #: :class:`Alias` object pointing at the disk image on which the 130 | #: target's volume resides *(optional)* 131 | self.disk_image_alias = disk_image_alias 132 | 133 | #: Dialup information (for automatic establishment of dialup connections) 134 | self.dialup_info = dialup_info 135 | 136 | #: Network mount information (for automatic remounting) 137 | self.network_mount_info = network_mount_info 138 | 139 | @property 140 | def filesystem_type(self): 141 | return ALIAS_FSTYPE_MAP.get(self.fs_type, ALIAS_FILESYSTEM_UNKNOWN) 142 | 143 | def __repr__(self): 144 | args = ['name', 'creation_date', 'fs_type', 'disk_type', 145 | 'attribute_flags', 'fs_id'] 146 | values = [] 147 | for a in args: 148 | v = getattr(self, a) 149 | values.append(repr(v)) 150 | 151 | kwargs = ['appleshare_info', 'driver_name', 'posix_path', 152 | 'disk_image_alias', 'dialup_info', 'network_mount_info'] 153 | for a in kwargs: 154 | v = getattr(self, a) 155 | if v is not None: 156 | values.append('%s=%r' % (a, v)) 157 | return 'VolumeInfo(%s)' % ','.join(values) 158 | 159 | class TargetInfo (object): 160 | def __init__(self, kind, filename, folder_cnid, cnid, creation_date, 161 | creator_code, type_code, levels_from=-1, levels_to=-1, 162 | folder_name=None, cnid_path=None, carbon_path=None, 163 | posix_path=None, user_home_prefix_len=None): 164 | #: Either ALIAS_KIND_FILE or ALIAS_KIND_FOLDER 165 | self.kind = kind 166 | 167 | #: The filename of the target 168 | self.filename = filename 169 | 170 | #: The CNID (Catalog Node ID) of the target's containing folder; 171 | #: CNIDs are similar to but different than traditional UNIX inode 172 | #: numbers 173 | self.folder_cnid = folder_cnid 174 | 175 | #: The CNID (Catalog Node ID) of the target 176 | self.cnid = cnid 177 | 178 | #: The target's *creation* date. 179 | self.creation_date = creation_date 180 | 181 | #: The target's Mac creator code (a four-character binary string) 182 | self.creator_code = creator_code 183 | 184 | #: The target's Mac type code (a four-character binary string) 185 | self.type_code = type_code 186 | 187 | #: The depth of the alias? Always seems to be -1 on OS X. 188 | self.levels_from = levels_from 189 | 190 | #: The depth of the target? Always seems to be -1 on OS X. 191 | self.levels_to = levels_to 192 | 193 | #: The (POSIX) name of the target's containing folder. *(optional)* 194 | self.folder_name = folder_name 195 | 196 | #: The path from the volume root as a sequence of CNIDs. *(optional)* 197 | self.cnid_path = cnid_path 198 | 199 | #: The Carbon path of the target *(optional)* 200 | self.carbon_path = carbon_path 201 | 202 | #: The POSIX path of the target relative to the volume root. Note 203 | #: that this may or may not have a leading '/' character, but it is 204 | #: always relative to the containing volume. *(optional)* 205 | self.posix_path = posix_path 206 | 207 | #: If the path points into a user's home folder, the number of folders 208 | #: deep that we go before we get to that home folder. *(optional)* 209 | self.user_home_prefix_len = user_home_prefix_len 210 | 211 | def __repr__(self): 212 | args = ['kind', 'filename', 'folder_cnid', 'cnid', 'creation_date', 213 | 'creator_code', 'type_code'] 214 | values = [] 215 | for a in args: 216 | v = getattr(self, a) 217 | values.append(repr(v)) 218 | 219 | if self.levels_from != -1: 220 | values.append('levels_from=%r' % self.levels_from) 221 | if self.levels_to != -1: 222 | values.append('levels_to=%r' % self.levels_to) 223 | 224 | kwargs = ['folder_name', 'cnid_path', 'carbon_path', 225 | 'posix_path', 'user_home_prefix_len'] 226 | for a in kwargs: 227 | v = getattr(self, a) 228 | values.append('%s=%r' % (a, v)) 229 | 230 | return 'TargetInfo(%s)' % ','.join(values) 231 | 232 | TAG_CARBON_FOLDER_NAME = 0 233 | TAG_CNID_PATH = 1 234 | TAG_CARBON_PATH = 2 235 | TAG_APPLESHARE_ZONE = 3 236 | TAG_APPLESHARE_SERVER_NAME = 4 237 | TAG_APPLESHARE_USERNAME = 5 238 | TAG_DRIVER_NAME = 6 239 | TAG_NETWORK_MOUNT_INFO = 9 240 | TAG_DIALUP_INFO = 10 241 | TAG_UNICODE_FILENAME = 14 242 | TAG_UNICODE_VOLUME_NAME = 15 243 | TAG_HIGH_RES_VOLUME_CREATION_DATE = 16 244 | TAG_HIGH_RES_CREATION_DATE = 17 245 | TAG_POSIX_PATH = 18 246 | TAG_POSIX_PATH_TO_MOUNTPOINT = 19 247 | TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE = 20 248 | TAG_USER_HOME_LENGTH_PREFIX = 21 249 | 250 | class Alias (object): 251 | def __init__(self, appinfo=b'\0\0\0\0', version=2, volume=None, 252 | target=None, extra=[]): 253 | """Construct a new :class:`Alias` object with the specified 254 | contents.""" 255 | 256 | #: Application specific information (four byte byte-string) 257 | self.appinfo = appinfo 258 | 259 | #: Version (we support versions 2 and 3) 260 | self.version = version 261 | 262 | #: A :class:`VolumeInfo` object describing the target's volume 263 | self.volume = volume 264 | 265 | #: A :class:`TargetInfo` object describing the target 266 | self.target = target 267 | 268 | #: A list of extra `(tag, value)` pairs 269 | self.extra = list(extra) 270 | 271 | @classmethod 272 | def _from_fd(cls, b): 273 | appinfo, recsize, version = struct.unpack(b'>4shh', b.read(8)) 274 | 275 | if recsize < 150: 276 | raise ValueError('Incorrect alias length') 277 | 278 | if version not in (2, 3): 279 | raise ValueError('Unsupported alias version %u' % version) 280 | 281 | if version == 2: 282 | kind, volname, voldate, fstype, disktype, \ 283 | folder_cnid, filename, cnid, crdate, creator_code, type_code, \ 284 | levels_from, levels_to, volattrs, volfsid, reserved = \ 285 | struct.unpack(b'>h28pI2shI64pII4s4shhI2s10s', b.read(142)) 286 | else: 287 | kind, voldate_hr, fstype, disktype, folder_cnid, cnid, crdate_hr, \ 288 | volattrs, reserved = \ 289 | struct.unpack(b'>hQ4shIIQI14s', b.read(46)) 290 | 291 | volname = b'' 292 | filename = b'' 293 | creator_code = None 294 | type_code = None 295 | voldate = voldate_hr / 65536.0 296 | crdate = crdate_hr / 65536.0 297 | 298 | voldate = mac_epoch + datetime.timedelta(seconds=voldate) 299 | crdate = mac_epoch + datetime.timedelta(seconds=crdate) 300 | 301 | alias = Alias() 302 | alias.appinfo = appinfo 303 | 304 | alias.volume = VolumeInfo (volname.decode().replace('/',':'), 305 | voldate, fstype, disktype, 306 | volattrs, volfsid) 307 | alias.target = TargetInfo (kind, filename.decode().replace('/',':'), 308 | folder_cnid, cnid, 309 | crdate, creator_code, type_code) 310 | alias.target.levels_from = levels_from 311 | alias.target.levels_to = levels_to 312 | 313 | tag = struct.unpack(b'>h', b.read(2))[0] 314 | 315 | while tag != -1: 316 | length = struct.unpack(b'>h', b.read(2))[0] 317 | value = b.read(length) 318 | if length & 1: 319 | b.read(1) 320 | 321 | if tag == TAG_CARBON_FOLDER_NAME: 322 | alias.target.folder_name = value.decode().replace('/',':') 323 | elif tag == TAG_CNID_PATH: 324 | alias.target.cnid_path = struct.unpack('>%uI' % (length // 4), 325 | value) 326 | elif tag == TAG_CARBON_PATH: 327 | alias.target.carbon_path = value 328 | elif tag == TAG_APPLESHARE_ZONE: 329 | if alias.volume.appleshare_info is None: 330 | alias.volume.appleshare_info = AppleShareInfo() 331 | alias.volume.appleshare_info.zone = value 332 | elif tag == TAG_APPLESHARE_SERVER_NAME: 333 | if alias.volume.appleshare_info is None: 334 | alias.volume.appleshare_info = AppleShareInfo() 335 | alias.volume.appleshare_info.server = value 336 | elif tag == TAG_APPLESHARE_USERNAME: 337 | if alias.volume.appleshare_info is None: 338 | alias.volume.appleshare_info = AppleShareInfo() 339 | alias.volume.appleshare_info.user = value 340 | elif tag == TAG_DRIVER_NAME: 341 | alias.volume.driver_name = value 342 | elif tag == TAG_NETWORK_MOUNT_INFO: 343 | alias.volume.network_mount_info = value 344 | elif tag == TAG_DIALUP_INFO: 345 | alias.volume.dialup_info = value 346 | elif tag == TAG_UNICODE_FILENAME: 347 | alias.target.filename = value[2:].decode('utf-16be') 348 | elif tag == TAG_UNICODE_VOLUME_NAME: 349 | alias.volume.name = value[2:].decode('utf-16be') 350 | elif tag == TAG_HIGH_RES_VOLUME_CREATION_DATE: 351 | seconds = struct.unpack(b'>Q', value)[0] / 65536.0 352 | alias.volume.creation_date \ 353 | = mac_epoch + datetime.timedelta(seconds=seconds) 354 | elif tag == TAG_HIGH_RES_CREATION_DATE: 355 | seconds = struct.unpack(b'>Q', value)[0] / 65536.0 356 | alias.target.creation_date \ 357 | = mac_epoch + datetime.timedelta(seconds=seconds) 358 | elif tag == TAG_POSIX_PATH: 359 | alias.target.posix_path = value.decode() 360 | elif tag == TAG_POSIX_PATH_TO_MOUNTPOINT: 361 | alias.volume.posix_path = value.decode() 362 | elif tag == TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE: 363 | alias.volume.disk_image_alias = Alias.from_bytes(value) 364 | elif tag == TAG_USER_HOME_LENGTH_PREFIX: 365 | alias.target.user_home_prefix_len = struct.unpack(b'>h', value)[0] 366 | else: 367 | alias.extra.append((tag, value)) 368 | 369 | tag = struct.unpack(b'>h', b.read(2))[0] 370 | 371 | return alias 372 | 373 | @classmethod 374 | def from_bytes(cls, bytes): 375 | """Construct an :class:`Alias` object given binary Alias data.""" 376 | with io.BytesIO(bytes) as b: 377 | return cls._from_fd(b) 378 | 379 | @classmethod 380 | def for_file(cls, path): 381 | """Create an :class:`Alias` that points at the specified file.""" 382 | if sys.platform != 'darwin': 383 | raise Exception('Not implemented (requires special support)') 384 | 385 | path = encode_utf8(path) 386 | 387 | a = Alias() 388 | 389 | # Find the filesystem 390 | st = osx.statfs(path) 391 | vol_path = st.f_mntonname 392 | 393 | # File and folder names in HFS+ are normalized to a form similar to NFD. 394 | # Must be normalized (NFD->NFC) before use to avoid unicode string comparison issues. 395 | vol_path = normalize("NFC", vol_path.decode('utf-8')).encode('utf-8') 396 | 397 | # Grab its attributes 398 | attrs = [osx.ATTR_CMN_CRTIME, 399 | osx.ATTR_VOL_NAME, 400 | 0, 0, 0] 401 | volinfo = osx.getattrlist(vol_path, attrs, 0) 402 | 403 | vol_crtime = volinfo[0] 404 | vol_name = encode_utf8(volinfo[1]) 405 | 406 | # Also grab various attributes of the file 407 | attrs = [(osx.ATTR_CMN_OBJTYPE 408 | | osx.ATTR_CMN_CRTIME 409 | | osx.ATTR_CMN_FNDRINFO 410 | | osx.ATTR_CMN_FILEID 411 | | osx.ATTR_CMN_PARENTID), 0, 0, 0, 0] 412 | info = osx.getattrlist(path, attrs, osx.FSOPT_NOFOLLOW) 413 | 414 | if info[0] == osx.VDIR: 415 | kind = ALIAS_KIND_FOLDER 416 | else: 417 | kind = ALIAS_KIND_FILE 418 | 419 | cnid = info[3] 420 | folder_cnid = info[4] 421 | 422 | dirname, filename = os.path.split(path) 423 | 424 | if dirname == b'' or dirname == b'.': 425 | dirname = os.getcwd() 426 | 427 | foldername = os.path.basename(dirname) 428 | 429 | creation_date = info[1] 430 | 431 | if kind == ALIAS_KIND_FILE: 432 | creator_code = struct.pack(b'I', info[2].fileInfo.fileCreator) 433 | type_code = struct.pack(b'I', info[2].fileInfo.fileType) 434 | else: 435 | creator_code = b'\0\0\0\0' 436 | type_code = b'\0\0\0\0' 437 | 438 | a.target = TargetInfo(kind, filename, folder_cnid, cnid, creation_date, 439 | creator_code, type_code) 440 | a.volume = VolumeInfo(vol_name, vol_crtime, b'H+', 441 | ALIAS_FIXED_DISK, 0, b'\0\0') 442 | 443 | a.target.folder_name = foldername 444 | a.volume.posix_path = vol_path 445 | 446 | rel_path = os.path.relpath(path, vol_path) 447 | 448 | # Leave off the initial '/' if vol_path is '/' (no idea why) 449 | if vol_path == b'/': 450 | a.target.posix_path = rel_path 451 | else: 452 | a.target.posix_path = b'/' + rel_path 453 | 454 | # Construct the Carbon and CNID paths 455 | carbon_path = [] 456 | cnid_path = [] 457 | head, tail = os.path.split(rel_path) 458 | if not tail: 459 | head, tail = os.path.split(head) 460 | while head or tail: 461 | if head: 462 | attrs = [osx.ATTR_CMN_FILEID, 0, 0, 0, 0] 463 | info = osx.getattrlist(os.path.join(vol_path, head), attrs, 0) 464 | cnid_path.append(info[0]) 465 | carbon_tail = tail.replace(b':',b'/') 466 | carbon_path.insert(0, carbon_tail) 467 | head, tail = os.path.split(head) 468 | 469 | carbon_path = vol_name + b':' + b':\0'.join(carbon_path) 470 | 471 | a.target.carbon_path = carbon_path 472 | a.target.cnid_path = cnid_path 473 | 474 | return a 475 | 476 | def _to_fd(self, b): 477 | # We'll come back and fix the length when we're done 478 | pos = b.tell() 479 | b.write(struct.pack(b'>4shh', self.appinfo, 0, self.version)) 480 | 481 | carbon_volname = encode_utf8(self.volume.name).replace(b':',b'/') 482 | carbon_filename = encode_utf8(self.target.filename).replace(b':',b'/') 483 | voldate = (self.volume.creation_date - mac_epoch).total_seconds() 484 | crdate = (self.target.creation_date - mac_epoch).total_seconds() 485 | 486 | if self.version == 2: 487 | # NOTE: crdate should be in local time, but that's system dependent 488 | # (so doing so is ridiculous, and nothing could rely on it). 489 | b.write(struct.pack(b'>h28pI2shI64pII4s4shhI2s10s', 490 | self.target.kind, 491 | carbon_volname, int(voldate), 492 | self.volume.fs_type, 493 | self.volume.disk_type, 494 | self.target.folder_cnid, 495 | carbon_filename, 496 | self.target.cnid, 497 | int(crdate), 498 | self.target.creator_code, 499 | self.target.type_code, 500 | self.target.levels_from, 501 | self.target.levels_to, 502 | self.volume.attribute_flags, 503 | self.volume.fs_id, 504 | b'\0'*10)) 505 | else: 506 | b.write(struct.pack(b'>hQ4shIIQI14s', 507 | self.target.kind, 508 | int(voldate * 65536), 509 | self.volume.fs_type, 510 | self.volume.disk_type, 511 | self.target.folder_cnid, 512 | self.target.cnid, 513 | int(crdate * 65536), 514 | self.volume.attribute_flags, 515 | self.volume.fs_id, 516 | b'\0'*14)) 517 | 518 | # Excuse the odd order; we're copying Finder 519 | if self.target.folder_name: 520 | carbon_foldername = encode_utf8(self.target.folder_name)\ 521 | .replace(b':',b'/') 522 | b.write(struct.pack(b'>hh', TAG_CARBON_FOLDER_NAME, 523 | len(carbon_foldername))) 524 | b.write(carbon_foldername) 525 | if len(carbon_foldername) & 1: 526 | b.write(b'\0') 527 | 528 | b.write(struct.pack(b'>hhQhhQ', 529 | TAG_HIGH_RES_VOLUME_CREATION_DATE, 530 | 8, int(voldate * 65536), 531 | TAG_HIGH_RES_CREATION_DATE, 532 | 8, int(crdate * 65536))) 533 | 534 | if self.target.cnid_path: 535 | cnid_path = struct.pack('>%uI' % len(self.target.cnid_path), 536 | *self.target.cnid_path) 537 | b.write(struct.pack(b'>hh', TAG_CNID_PATH, 538 | len(cnid_path))) 539 | b.write(cnid_path) 540 | 541 | if self.target.carbon_path: 542 | carbon_path=encode_utf8(self.target.carbon_path) 543 | b.write(struct.pack(b'>hh', TAG_CARBON_PATH, 544 | len(carbon_path))) 545 | b.write(carbon_path) 546 | if len(carbon_path) & 1: 547 | b.write(b'\0') 548 | 549 | if self.volume.appleshare_info: 550 | ai = self.volume.appleshare_info 551 | if ai.zone: 552 | b.write(struct.pack(b'>hh', TAG_APPLESHARE_ZONE, 553 | len(ai.zone))) 554 | b.write(ai.zone) 555 | if len(ai.zone) & 1: 556 | b.write(b'\0') 557 | if ai.server: 558 | b.write(struct.pack(b'>hh', TAG_APPLESHARE_SERVER_NAME, 559 | len(ai.server))) 560 | b.write(ai.server) 561 | if len(ai.server) & 1: 562 | b.write(b'\0') 563 | if ai.username: 564 | b.write(struct.pack(b'>hh', TAG_APPLESHARE_USERNAME, 565 | len(ai.username))) 566 | b.write(ai.username) 567 | if len(ai.username) & 1: 568 | b.write(b'\0') 569 | 570 | if self.volume.driver_name: 571 | driver_name = encode_utf8(self.volume.driver_name) 572 | b.write(struct.pack(b'>hh', TAG_DRIVER_NAME, 573 | len(driver_name))) 574 | b.write(driver_name) 575 | if len(driver_name) & 1: 576 | b.write(b'\0') 577 | 578 | if self.volume.network_mount_info: 579 | b.write(struct.pack(b'>hh', TAG_NETWORK_MOUNT_INFO, 580 | len(self.volume.network_mount_info))) 581 | b.write(self.volume.network_mount_info) 582 | if len(self.volume.network_mount_info) & 1: 583 | b.write(b'\0') 584 | 585 | if self.volume.dialup_info: 586 | b.write(struct.pack(b'>hh', TAG_DIALUP_INFO, 587 | len(self.volume.network_mount_info))) 588 | b.write(self.volume.network_mount_info) 589 | if len(self.volume.network_mount_info) & 1: 590 | b.write(b'\0') 591 | 592 | utf16 = decode_utf8(self.target.filename)\ 593 | .replace(':','/').encode('utf-16-be') 594 | b.write(struct.pack(b'>hhh', TAG_UNICODE_FILENAME, 595 | len(utf16) + 2, 596 | len(utf16) // 2)) 597 | b.write(utf16) 598 | 599 | utf16 = decode_utf8(self.volume.name)\ 600 | .replace(':','/').encode('utf-16-be') 601 | b.write(struct.pack(b'>hhh', TAG_UNICODE_VOLUME_NAME, 602 | len(utf16) + 2, 603 | len(utf16) // 2)) 604 | b.write(utf16) 605 | 606 | if self.target.posix_path: 607 | posix_path = encode_utf8(self.target.posix_path) 608 | b.write(struct.pack(b'>hh', TAG_POSIX_PATH, 609 | len(posix_path))) 610 | b.write(posix_path) 611 | if len(posix_path) & 1: 612 | b.write(b'\0') 613 | 614 | if self.volume.posix_path: 615 | posix_path = encode_utf8(self.volume.posix_path) 616 | b.write(struct.pack(b'>hh', TAG_POSIX_PATH_TO_MOUNTPOINT, 617 | len(posix_path))) 618 | b.write(posix_path) 619 | if len(posix_path) & 1: 620 | b.write(b'\0') 621 | 622 | if self.volume.disk_image_alias: 623 | d = self.volume.disk_image_alias.to_bytes() 624 | b.write(struct.pack(b'>hh', TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE, 625 | len(d))) 626 | b.write(d) 627 | if len(d) & 1: 628 | b.write(b'\0') 629 | 630 | if self.target.user_home_prefix_len is not None: 631 | b.write(struct.pack(b'>hhh', TAG_USER_HOME_LENGTH_PREFIX, 632 | 2, self.target.user_home_prefix_len)) 633 | 634 | for t,v in self.extra: 635 | b.write(struct.pack(b'>hh', t, len(v))) 636 | b.write(v) 637 | if len(v) & 1: 638 | b.write(b'\0') 639 | 640 | b.write(struct.pack(b'>hh', -1, 0)) 641 | 642 | blen = b.tell() - pos 643 | b.seek(pos + 4, os.SEEK_SET) 644 | b.write(struct.pack(b'>h', blen)) 645 | 646 | def to_bytes(self): 647 | """Returns the binary representation for this :class:`Alias`.""" 648 | with io.BytesIO() as b: 649 | self._to_fd(b) 650 | return b.getvalue() 651 | 652 | def __str__(self): 653 | return '' % self.target.filename 654 | 655 | def __repr__(self): 656 | values = [] 657 | if self.appinfo != b'\0\0\0\0': 658 | values.append('appinfo=%r' % self.appinfo) 659 | if self.version != 2: 660 | values.append('version=%r' % self.version) 661 | if self.volume is not None: 662 | values.append('volume=%r' % self.volume) 663 | if self.target is not None: 664 | values.append('target=%r' % self.target) 665 | if self.extra: 666 | values.append('extra=%r' % self.extra) 667 | return 'Alias(%s)' % ','.join(values) 668 | -------------------------------------------------------------------------------- /mac_alias/bookmark.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This file implements the Apple "bookmark" format, which is the replacement 4 | # for the old-fashioned alias format. The details of this format were 5 | # reverse engineered; some things are still not entirely clear. 6 | # 7 | from __future__ import unicode_literals, print_function 8 | 9 | import struct 10 | import uuid 11 | import datetime 12 | import os 13 | import sys 14 | import pprint 15 | 16 | try: 17 | from urlparse import urljoin 18 | except ImportError: 19 | from urllib.parse import urljoin 20 | 21 | if sys.platform == 'darwin': 22 | from . import osx 23 | 24 | def iteritems(x): 25 | return x.iteritems() 26 | 27 | try: 28 | unicode 29 | except NameError: 30 | unicode = str 31 | long = int 32 | xrange = range 33 | def iteritems(x): 34 | return x.items() 35 | 36 | from .utils import * 37 | 38 | BMK_DATA_TYPE_MASK = 0xffffff00 39 | BMK_DATA_SUBTYPE_MASK = 0x000000ff 40 | 41 | BMK_STRING = 0x0100 42 | BMK_DATA = 0x0200 43 | BMK_NUMBER = 0x0300 44 | BMK_DATE = 0x0400 45 | BMK_BOOLEAN = 0x0500 46 | BMK_ARRAY = 0x0600 47 | BMK_DICT = 0x0700 48 | BMK_UUID = 0x0800 49 | BMK_URL = 0x0900 50 | BMK_NULL = 0x0a00 51 | 52 | BMK_ST_ZERO = 0x0000 53 | BMK_ST_ONE = 0x0001 54 | 55 | BMK_BOOLEAN_ST_FALSE = 0x0000 56 | BMK_BOOLEAN_ST_TRUE = 0x0001 57 | 58 | # Subtypes for BMK_NUMBER are really CFNumberType values 59 | kCFNumberSInt8Type = 1 60 | kCFNumberSInt16Type = 2 61 | kCFNumberSInt32Type = 3 62 | kCFNumberSInt64Type = 4 63 | kCFNumberFloat32Type = 5 64 | kCFNumberFloat64Type = 6 65 | kCFNumberCharType = 7 66 | kCFNumberShortType = 8 67 | kCFNumberIntType = 9 68 | kCFNumberLongType = 10 69 | kCFNumberLongLongType = 11 70 | kCFNumberFloatType = 12 71 | kCFNumberDoubleType = 13 72 | kCFNumberCFIndexType = 14 73 | kCFNumberNSIntegerType = 15 74 | kCFNumberCGFloatType = 16 75 | 76 | # Resource property flags (from CFURLPriv.h) 77 | kCFURLResourceIsRegularFile = 0x00000001 78 | kCFURLResourceIsDirectory = 0x00000002 79 | kCFURLResourceIsSymbolicLink = 0x00000004 80 | kCFURLResourceIsVolume = 0x00000008 81 | kCFURLResourceIsPackage = 0x00000010 82 | kCFURLResourceIsSystemImmutable = 0x00000020 83 | kCFURLResourceIsUserImmutable = 0x00000040 84 | kCFURLResourceIsHidden = 0x00000080 85 | kCFURLResourceHasHiddenExtension = 0x00000100 86 | kCFURLResourceIsApplication = 0x00000200 87 | kCFURLResourceIsCompressed = 0x00000400 88 | kCFURLResourceIsSystemCompressed = 0x00000400 89 | kCFURLCanSetHiddenExtension = 0x00000800 90 | kCFURLResourceIsReadable = 0x00001000 91 | kCFURLResourceIsWriteable = 0x00002000 92 | kCFURLResourceIsExecutable = 0x00004000 93 | kCFURLIsAliasFile = 0x00008000 94 | kCFURLIsMountTrigger = 0x00010000 95 | 96 | # Volume property flags (from CFURLPriv.h) 97 | kCFURLVolumeIsLocal = 0x1 # 98 | kCFURLVolumeIsAutomount = 0x2 # 99 | kCFURLVolumeDontBrowse = 0x4 # 100 | kCFURLVolumeIsReadOnly = 0x8 # 101 | kCFURLVolumeIsQuarantined = 0x10 102 | kCFURLVolumeIsEjectable = 0x20 # 103 | kCFURLVolumeIsRemovable = 0x40 # 104 | kCFURLVolumeIsInternal = 0x80 # 105 | kCFURLVolumeIsExternal = 0x100 # 106 | kCFURLVolumeIsDiskImage = 0x200 # 107 | kCFURLVolumeIsFileVault = 0x400 108 | kCFURLVolumeIsLocaliDiskMirror = 0x800 109 | kCFURLVolumeIsiPod = 0x1000 # 110 | kCFURLVolumeIsiDisk = 0x2000 111 | kCFURLVolumeIsCD = 0x4000 112 | kCFURLVolumeIsDVD = 0x8000 113 | kCFURLVolumeIsDeviceFileSystem = 0x10000 114 | kCFURLVolumeSupportsPersistentIDs = 0x100000000 115 | kCFURLVolumeSupportsSearchFS = 0x200000000 116 | kCFURLVolumeSupportsExchange = 0x400000000 117 | # reserved 0x800000000 118 | kCFURLVolumeSupportsSymbolicLinks = 0x1000000000 119 | kCFURLVolumeSupportsDenyModes = 0x2000000000 120 | kCFURLVolumeSupportsCopyFile = 0x4000000000 121 | kCFURLVolumeSupportsReadDirAttr = 0x8000000000 122 | kCFURLVolumeSupportsJournaling = 0x10000000000 123 | kCFURLVolumeSupportsRename = 0x20000000000 124 | kCFURLVolumeSupportsFastStatFS = 0x40000000000 125 | kCFURLVolumeSupportsCaseSensitiveNames = 0x80000000000 126 | kCFURLVolumeSupportsCasePreservedNames = 0x100000000000 127 | kCFURLVolumeSupportsFLock = 0x200000000000 128 | kCFURLVolumeHasNoRootDirectoryTimes = 0x400000000000 129 | kCFURLVolumeSupportsExtendedSecurity = 0x800000000000 130 | kCFURLVolumeSupports2TBFileSize = 0x1000000000000 131 | kCFURLVolumeSupportsHardLinks = 0x2000000000000 132 | kCFURLVolumeSupportsMandatoryByteRangeLocks = 0x4000000000000 133 | kCFURLVolumeSupportsPathFromID = 0x8000000000000 134 | # reserved 0x10000000000000 135 | kCFURLVolumeIsJournaling = 0x20000000000000 136 | kCFURLVolumeSupportsSparseFiles = 0x40000000000000 137 | kCFURLVolumeSupportsZeroRuns = 0x80000000000000 138 | kCFURLVolumeSupportsVolumeSizes = 0x100000000000000 139 | kCFURLVolumeSupportsRemoteEvents = 0x200000000000000 140 | kCFURLVolumeSupportsHiddenFiles = 0x400000000000000 141 | kCFURLVolumeSupportsDecmpFSCompression = 0x800000000000000 142 | kCFURLVolumeHas64BitObjectIDs = 0x1000000000000000 143 | kCFURLVolumePropertyFlagsAll = 0xffffffffffffffff 144 | 145 | BMK_URL_ST_ABSOLUTE = 0x0001 146 | BMK_URL_ST_RELATIVE = 0x0002 147 | 148 | # Bookmark keys 149 | kBookmarkURL = 0x1003 # A URL 150 | kBookmarkPath = 0x1004 # Array of path components 151 | kBookmarkCNIDPath = 0x1005 # Array of CNIDs 152 | kBookmarkFileProperties = 0x1010 # (CFURL rp flags, 153 | # CFURL rp flags asked for, 154 | # 8 bytes NULL) 155 | kBookmarkFileName = 0x1020 156 | kBookmarkFileID = 0x1030 157 | kBookmarkFileCreationDate = 0x1040 158 | # = 0x1054 # ? 159 | # = 0x1055 # ? 160 | # = 0x1056 # ? 161 | # = 0x1101 # ? 162 | # = 0x1102 # ? 163 | kBookmarkTOCPath = 0x2000 # A list of (TOC id, ?) pairs 164 | kBookmarkVolumePath = 0x2002 165 | kBookmarkVolumeURL = 0x2005 166 | kBookmarkVolumeName = 0x2010 167 | kBookmarkVolumeUUID = 0x2011 # Stored (perversely) as a string 168 | kBookmarkVolumeSize = 0x2012 169 | kBookmarkVolumeCreationDate = 0x2013 170 | kBookmarkVolumeProperties = 0x2020 # (CFURL vp flags, 171 | # CFURL vp flags asked for, 172 | # 8 bytes NULL) 173 | kBookmarkVolumeIsRoot = 0x2030 # True if volume is FS root 174 | kBookmarkVolumeBookmark = 0x2040 # Embedded bookmark for disk image (TOC id) 175 | kBookmarkVolumeMountPoint = 0x2050 # A URL 176 | # = 0x2070 177 | kBookmarkContainingFolder = 0xc001 # Index of containing folder in path 178 | kBookmarkUserName = 0xc011 # User that created bookmark 179 | kBookmarkUID = 0xc012 # UID that created bookmark 180 | kBookmarkWasFileReference = 0xd001 # True if the URL was a file reference 181 | kBookmarkCreationOptions = 0xd010 182 | kBookmarkURLLengths = 0xe003 # See below 183 | kBookmarkDisplayName = 0xf017 184 | kBookmarkIconData = 0xf020 185 | kBookmarkIconRef = 0xf021 186 | kBookmarkTypeBindingData = 0xf022 187 | kBookmarkCreationTime = 0xf030 188 | kBookmarkSandboxRwExtension = 0xf080 189 | kBookmarkSandboxRoExtension = 0xf081 190 | kBookmarkAliasData = 0xfe00 191 | 192 | # Alias for backwards compatibility 193 | kBookmarkSecurityExtension = kBookmarkSandboxRwExtension 194 | 195 | # kBookmarkURLLengths is an array that is set if the URL encoded by the 196 | # bookmark had a base URL; in that case, each entry is the length of the 197 | # base URL in question. Thus a URL 198 | # 199 | # file:///foo/bar/baz blam/blat.html 200 | # 201 | # will result in [3, 2], while the URL 202 | # 203 | # file:///foo bar/baz blam blat.html 204 | # 205 | # would result in [1, 2, 1, 1] 206 | 207 | 208 | class Data (object): 209 | def __init__(self, bytedata=None): 210 | #: The bytes, stored as a byte string 211 | self.bytes = bytes(bytedata) 212 | 213 | def __repr__(self): 214 | return 'Data(%r)' % self.bytes 215 | 216 | class URL (object): 217 | def __init__(self, base, rel=None): 218 | if rel is not None: 219 | #: The base URL, if any (a :class:`URL`) 220 | self.base = base 221 | #: The rest of the URL (a string) 222 | self.relative = rel 223 | else: 224 | self.base = None 225 | self.relative = base 226 | 227 | @property 228 | def absolute(self): 229 | """Return an absolute URL.""" 230 | if self.base is None: 231 | return self.relative 232 | else: 233 | base_abs = self.base.absolute 234 | return urljoin(self.base.absolute, self.relative) 235 | 236 | def __repr__(self): 237 | return 'URL(%r)' % self.absolute 238 | 239 | class Bookmark (object): 240 | def __init__(self, tocs=None): 241 | if tocs is None: 242 | #: The TOCs for this Bookmark 243 | self.tocs = [] 244 | else: 245 | self.tocs = tocs 246 | 247 | @classmethod 248 | def _get_item(cls, data, hdrsize, offset): 249 | offset += hdrsize 250 | if offset > len(data) - 8: 251 | raise ValueError('Offset out of range') 252 | 253 | length,typecode = struct.unpack(b'd', databytes)[0]) 284 | return osx_epoch + secs 285 | elif dtype == BMK_BOOLEAN: 286 | if dsubtype == BMK_BOOLEAN_ST_TRUE: 287 | return True 288 | elif dsubtype == BMK_BOOLEAN_ST_FALSE: 289 | return False 290 | elif dtype == BMK_UUID: 291 | return uuid.UUID(bytes=databytes) 292 | elif dtype == BMK_URL: 293 | if dsubtype == BMK_URL_ST_ABSOLUTE: 294 | return URL(databytes.decode('utf-8')) 295 | elif dsubtype == BMK_URL_ST_RELATIVE: 296 | baseoff,reloff = struct.unpack(b' size: 339 | raise ValueError('Not a bookmark file (header size too large)') 340 | 341 | if size != len(data): 342 | raise ValueError('Not a bookmark file (truncated)') 343 | 344 | tocoffset, = struct.unpack(b' size - hdrsize \ 351 | or size - tocbase < 20: 352 | raise ValueError('TOC offset out of range') 353 | 354 | tocsize,tocmagic,tocid,nexttoc,toccount \ 355 | = struct.unpack(b' -0x80000000 and item < 0x7fffffff: 428 | result = struct.pack(b'd', float(secs.total_seconds())) 440 | elif isinstance(item, uuid.UUID): 441 | result = struct.pack(b' -1: 512 | sz = sz[:nul] 513 | return sz.decode('utf-8') 514 | 515 | def _decode_attrlist_result(buf, attrs, options): 516 | result = [] 517 | 518 | assert len(buf) >= 4 519 | total_size = uint32_t.from_buffer(buf, 0).value 520 | assert total_size <= len(buf) 521 | 522 | offset = 4 523 | 524 | # Common attributes 525 | if attrs[0] & ATTR_CMN_RETURNED_ATTRS: 526 | a = attribute_set_t.from_buffer(buf, offset) 527 | result.append(a) 528 | offset += sizeof (attribute_set_t) 529 | if not (options & FSOPT_PACK_INVAL_ATTRS): 530 | attrs = [a.commonattr, a.volattr, a.dirattr, a.fileattr, a.forkattr] 531 | if attrs[0] & ATTR_CMN_NAME: 532 | a = attrreference_t.from_buffer(buf, offset) 533 | ofs = offset + a.attr_dataoffset 534 | name = _decode_utf8_nul(buf[ofs:ofs+a.attr_length]) 535 | offset += sizeof (attrreference_t) 536 | result.append(name) 537 | if attrs[0] & ATTR_CMN_DEVID: 538 | a = dev_t.from_buffer(buf, offset) 539 | offset += sizeof(dev_t) 540 | result.append(a.value) 541 | if attrs[0] & ATTR_CMN_FSID: 542 | a = fsid_t.from_buffer(buf, offset) 543 | offset += sizeof(fsid_t) 544 | result.append(a) 545 | if attrs[0] & ATTR_CMN_OBJTYPE: 546 | a = fsobj_type_t.from_buffer(buf, offset) 547 | offset += sizeof(fsobj_type_t) 548 | result.append(a.value) 549 | if attrs[0] & ATTR_CMN_OBJTAG: 550 | a = fsobj_tag_t.from_buffer(buf, offset) 551 | offset += sizeof(fsobj_tag_t) 552 | result.append(a.value) 553 | if attrs[0] & ATTR_CMN_OBJID: 554 | a = fsobj_id_t.from_buffer(buf, offset) 555 | offset += sizeof(fsobj_id_t) 556 | result.append(a) 557 | if attrs[0] & ATTR_CMN_OBJPERMANENTID: 558 | a = fsobj_id_t.from_buffer(buf, offset) 559 | offset += sizeof(fsobj_id_t) 560 | result.append(a) 561 | if attrs[0] & ATTR_CMN_PAROBJID: 562 | a = fsobj_id_t.from_buffer(buf, offset) 563 | offset += sizeof(fsobj_id_t) 564 | result.append(a) 565 | if attrs[0] & ATTR_CMN_SCRIPT: 566 | a = text_encoding_t.from_buffer(buf, offset) 567 | offset += sizeof(text_encoding_t) 568 | result.append(a.value) 569 | if attrs[0] & ATTR_CMN_CRTIME: 570 | a = timespec.from_buffer(buf, offset) 571 | offset += sizeof(timespec) 572 | result.append(_datetime_from_timespec(a)) 573 | if attrs[0] & ATTR_CMN_MODTIME: 574 | a = timespec.from_buffer(buf, offset) 575 | offset += sizeof(timespec) 576 | result.append(_datetime_from_timespec(a)) 577 | if attrs[0] & ATTR_CMN_CHGTIME: 578 | a = timespec.from_buffer(buf, offset) 579 | offset += sizeof(timespec) 580 | result.append(_datetime_from_timespec(a)) 581 | if attrs[0] & ATTR_CMN_ACCTIME: 582 | a = timespec.from_buffer(buf, offset) 583 | offset += sizeof(timespec) 584 | result.append(_datetime_from_timespec(a)) 585 | if attrs[0] & ATTR_CMN_BKUPTIME: 586 | a = timespec.from_buffer(buf, offset) 587 | offset += sizeof(timespec) 588 | result.append(_datetime_from_timespec(a)) 589 | if attrs[0] & ATTR_CMN_FNDRINFO: 590 | a = FinderInfo.from_buffer(buf, offset) 591 | offset += sizeof(FinderInfo) 592 | result.append(a) 593 | if attrs[0] & ATTR_CMN_OWNERID: 594 | a = uid_t.from_buffer(buf, offset) 595 | offset += sizeof(uid_t) 596 | result.append(a.value) 597 | if attrs[0] & ATTR_CMN_GRPID: 598 | a = gid_t.from_buffer(buf, offset) 599 | offset += sizeof(gid_t) 600 | result.append(a.value) 601 | if attrs[0] & ATTR_CMN_ACCESSMASK: 602 | a = uint32_t.from_buffer(buf, offset) 603 | offset += sizeof(uint32_t) 604 | result.append(a.value) 605 | if attrs[0] & ATTR_CMN_FLAGS: 606 | a = uint32_t.from_buffer(buf, offset) 607 | offset += sizeof(uint32_t) 608 | result.append(a.value) 609 | if attrs[0] & ATTR_CMN_GEN_COUNT: 610 | a = uint32_t.from_buffer(buf, offset) 611 | offset += sizeof(uint32_t) 612 | result.append(a.value) 613 | if attrs[0] & ATTR_CMN_DOCUMENT_ID: 614 | a = uint32_t.from_buffer(buf, offset) 615 | offset += sizeof(uint32_t) 616 | result.append(a.value) 617 | if attrs[0] & ATTR_CMN_USERACCESS: 618 | a = uint32_t.from_buffer(buf, offset) 619 | offset += sizeof(uint32_t) 620 | result.append(a.value) 621 | if attrs[0] & ATTR_CMN_EXTENDED_SECURITY: 622 | a = attrreference_t.from_buffer(buf, offset) 623 | ofs = offset + a.attr_dataoffset 624 | offset += sizeof(attrreference_t) 625 | ec = uint32_t.from_buffer(buf, ofs + 36).value 626 | class kauth_acl(Structure): 627 | _fields_ = [('acl_entrycount', c_uint), 628 | ('acl_flags', c_uint), 629 | ('acl_ace', kauth_ace * ec)] 630 | class kauth_filesec(Structure): 631 | _fields_ = [('fsec_magic', c_uint), 632 | ('fsec_owner', guid_t), 633 | ('fsec_group', guid_t), 634 | ('fsec_acl', kauth_acl)] 635 | a = kauth_filesec.from_buffer(buf, ofs) 636 | result.append(a) 637 | if attrs[0] & ATTR_CMN_UUID: 638 | result.append(uuid.UUID(bytes=buf[offset:offset+16])) 639 | offset += sizeof(guid_t) 640 | if attrs[0] & ATTR_CMN_GRPUUID: 641 | result.append(uuid.UUID(bytes=buf[offset:offset+16])) 642 | offset += sizeof(guid_t) 643 | if attrs[0] & ATTR_CMN_FILEID: 644 | a = uint64_t.from_buffer(buf, offset) 645 | offset += sizeof(uint64_t) 646 | result.append(a.value) 647 | if attrs[0] & ATTR_CMN_PARENTID: 648 | a = uint64_t.from_buffer(buf, offset) 649 | offset += sizeof(uint64_t) 650 | result.append(a.value) 651 | if attrs[0] & ATTR_CMN_FULLPATH: 652 | a = attrreference_t.from_buffer(buf, offset) 653 | ofs = offset + a.attr_dataoffset 654 | path = _decode_utf8_nul(buf[ofs:ofs+a.attr_length]) 655 | offset += sizeof (attrreference_t) 656 | result.append(path) 657 | if attrs[0] & ATTR_CMN_ADDEDTIME: 658 | a = timespec.from_buffer(buf, offset) 659 | offset += sizeof(timespec) 660 | result.append(_datetime_from_timespec(a)) 661 | if attrs[0] & ATTR_CMN_DATA_PROTECT_FLAGS: 662 | a = uint32_t.from_buffer(buf, offset) 663 | offset += sizeof(uint32_t) 664 | result.append(a.value) 665 | 666 | # Volume attributes 667 | if attrs[1] & ATTR_VOL_FSTYPE: 668 | a = uint32_t.from_buffer(buf, offset) 669 | offset += sizeof(uint32_t) 670 | result.append(a.value) 671 | if attrs[1] & ATTR_VOL_SIGNATURE: 672 | a = uint32_t.from_buffer(buf, offset) 673 | offset += sizeof(uint32_t) 674 | result.append(a.value) 675 | if attrs[1] & ATTR_VOL_SIZE: 676 | a = off_t.from_buffer(buf, offset) 677 | offset += sizeof(off_t) 678 | result.append(a.value) 679 | if attrs[1] & ATTR_VOL_SPACEFREE: 680 | a = off_t.from_buffer(buf, offset) 681 | offset += sizeof(off_t) 682 | result.append(a.value) 683 | if attrs[1] & ATTR_VOL_SPACEAVAIL: 684 | a = off_t.from_buffer(buf, offset) 685 | offset += sizeof(off_t) 686 | result.append(a.value) 687 | if attrs[1] & ATTR_VOL_MINALLOCATION: 688 | a = off_t.from_buffer(buf, offset) 689 | offset += sizeof(off_t) 690 | result.append(a.value) 691 | if attrs[1] & ATTR_VOL_ALLOCATIONCLUMP: 692 | a = off_t.from_buffer(buf, offset) 693 | offset += sizeof(off_t) 694 | result.append(a.value) 695 | if attrs[1] & ATTR_VOL_IOBLOCKSIZE: 696 | a = uint32_t.from_buffer(buf, offset) 697 | offset += sizeof(uint32_t) 698 | result.append(a.value) 699 | if attrs[1] & ATTR_VOL_OBJCOUNT: 700 | a = uint32_t.from_buffer(buf, offset) 701 | offset += sizeof(uint32_t) 702 | result.append(a.value) 703 | if attrs[1] & ATTR_VOL_FILECOUNT: 704 | a = uint32_t.from_buffer(buf, offset) 705 | offset += sizeof(uint32_t) 706 | result.append(a.value) 707 | if attrs[1] & ATTR_VOL_DIRCOUNT: 708 | a = uint32_t.from_buffer(buf, offset) 709 | offset += sizeof(uint32_t) 710 | result.append(a.value) 711 | if attrs[1] & ATTR_VOL_MAXOBJCOUNT: 712 | a = uint32_t.from_buffer(buf, offset) 713 | offset += sizeof(uint32_t) 714 | result.append(a.value) 715 | if attrs[1] & ATTR_VOL_MOUNTPOINT: 716 | a = attrreference_t.from_buffer(buf, offset) 717 | ofs = offset + a.attr_dataoffset 718 | path = _decode_utf8_nul(buf[ofs:ofs+a.attr_length]) 719 | offset += sizeof (attrreference_t) 720 | result.append(path) 721 | if attrs[1] & ATTR_VOL_NAME: 722 | a = attrreference_t.from_buffer(buf, offset) 723 | ofs = offset + a.attr_dataoffset 724 | name = _decode_utf8_nul(buf[ofs:ofs+a.attr_length]) 725 | offset += sizeof (attrreference_t) 726 | result.append(name) 727 | if attrs[1] & ATTR_VOL_MOUNTFLAGS: 728 | a = uint32_t.from_buffer(buf, offset) 729 | offset += sizeof(uint32_t) 730 | result.append(a.value) 731 | if attrs[1] & ATTR_VOL_MOUNTEDDEVICE: 732 | a = attrreference_t.from_buffer(buf, offset) 733 | ofs = offset + a.attr_dataoffset 734 | path = _decode_utf8_nul(buf[ofs:ofs+a.attr_length]) 735 | offset += sizeof (attrreference_t) 736 | result.append(path) 737 | if attrs[1] & ATTR_VOL_ENCODINGSUSED: 738 | a = c_ulonglong.from_buffer(buf, offset) 739 | offset += sizeof(c_ulonglong) 740 | result.append(a.value) 741 | if attrs[1] & ATTR_VOL_CAPABILITIES: 742 | a = vol_capabilities_attr_t.from_buffer(buf, offset) 743 | offset += sizeof(vol_capabilities_attr_t) 744 | result.append(a) 745 | if attrs[1] & ATTR_VOL_UUID: 746 | result.append(uuid.UUID(bytes=buf[offset:offset+16])) 747 | offset += sizeof(uuid_t) 748 | if attrs[1] & ATTR_VOL_QUOTA_SIZE: 749 | a = off_t.from_buffer(buf, offset) 750 | offset += sizeof(off_t) 751 | result.append(a.value) 752 | if attrs[1] & ATTR_VOL_RESERVED_SIZE: 753 | a = off_t.from_buffer(buf, offset) 754 | offset += sizeof(off_t) 755 | result.append(a.value) 756 | if attrs[1] & ATTR_VOL_ATTRIBUTES: 757 | a = vol_attributes_attr_t.from_buffer(buf, offset) 758 | offset += sizeof(vol_attributes_attr_t) 759 | result.append(a) 760 | 761 | # Directory attributes 762 | if attrs[2] & ATTR_DIR_LINKCOUNT: 763 | a = uint32_t.from_buffer(buf, offset) 764 | offset += sizeof(uint32_t) 765 | result.append(a.value) 766 | if attrs[2] & ATTR_DIR_ENTRYCOUNT: 767 | a = uint32_t.from_buffer(buf, offset) 768 | offset += sizeof(uint32_t) 769 | result.append(a.value) 770 | if attrs[2] & ATTR_DIR_MOUNTSTATUS: 771 | a = uint32_t.from_buffer(buf, offset) 772 | offset += sizeof(uint32_t) 773 | result.append(a.value) 774 | if attrs[2] & ATTR_DIR_ALLOCSIZE: 775 | a = off_t.from_buffer(buf, offset) 776 | offset += sizeof(off_t) 777 | result.append(a.value) 778 | if attrs[2] & ATTR_DIR_IOBLOCKSIZE: 779 | a = uint32_t.from_buffer(buf, offset) 780 | offset += sizeof(uint32_t) 781 | result.append(a.value) 782 | if attrs[2] & ATTR_DIR_DATALENGTH: 783 | a = off_t.from_buffer(buf, offset) 784 | offset += sizeof(off_t) 785 | result.append(a.value) 786 | 787 | # File attributes 788 | if attrs[3] & ATTR_FILE_LINKCOUNT: 789 | a = uint32_t.from_buffer(buf, offset) 790 | offset += sizeof(uint32_t) 791 | result.append(a.value) 792 | if attrs[3] & ATTR_FILE_TOTALSIZE: 793 | a = off_t.from_buffer(buf, offset) 794 | offset += sizeof(off_t) 795 | result.append(a.value) 796 | if attrs[3] & ATTR_FILE_ALLOCSIZE: 797 | a = off_t.from_buffer(buf, offset) 798 | offset += sizeof(off_t) 799 | result.append(a.value) 800 | if attrs[3] & ATTR_FILE_IOBLOCKSIZE: 801 | a = uint32_t.from_buffer(buf, offset) 802 | offset += sizeof(uint32_t) 803 | result.append(a.value) 804 | if attrs[3] & ATTR_FILE_CLUMPSIZE: 805 | a = uint32_t.from_buffer(buf, offset) 806 | offset += sizeof(uint32_t) 807 | result.append(a.value) 808 | if attrs[3] & ATTR_FILE_DEVTYPE: 809 | a = uint32_t.from_buffer(buf, offset) 810 | offset += sizeof(uint32_t) 811 | result.append(a.value) 812 | if attrs[3] & ATTR_FILE_FILETYPE: 813 | a = uint32_t.from_buffer(buf, offset) 814 | offset += sizeof(uint32_t) 815 | result.append(a.value) 816 | if attrs[3] & ATTR_FILE_FORKCOUNT: 817 | a = uint32_t.from_buffer(buf, offset) 818 | offset += sizeof(uint32_t) 819 | result.append(a.value) 820 | if attrs[3] & ATTR_FILE_DATALENGTH: 821 | a = off_t.from_buffer(buf, offset) 822 | offset += sizeof(off_t) 823 | result.append(a.value) 824 | if attrs[3] & ATTR_FILE_DATAALLOCSIZE: 825 | a = off_t.from_buffer(buf, offset) 826 | offset += sizeof(off_t) 827 | result.append(a.value) 828 | if attrs[3] & ATTR_FILE_DATAEXTENTS: 829 | a = extentrecord.from_buffer(buf, offset) 830 | offset += sizeof(extentrecord) 831 | result.append(a.value) 832 | if attrs[3] & ATTR_FILE_RSRCLENGTH: 833 | a = off_t.from_buffer(buf, offset) 834 | offset += sizeof(off_t) 835 | result.append(a.value) 836 | if attrs[3] & ATTR_FILE_RSRCALLOCSIZE: 837 | a = off_t.from_buffer(buf, offset) 838 | offset += sizeof(off_t) 839 | result.append(a.value) 840 | if attrs[3] & ATTR_FILE_RSRCEXTENTS: 841 | a = extentrecord.from_buffer(buf, offset) 842 | offset += sizeof(extentrecord) 843 | result.append(a.value) 844 | 845 | # Fork attributes 846 | if attrs[4] & ATTR_FORK_TOTALSIZE: 847 | a = off_t.from_buffer(buf, offset) 848 | offset += sizeof(off_t) 849 | result.append(a.value) 850 | if attrs[4] & ATTR_FORK_ALLOCSIZE: 851 | a = off_t.from_buffer(buf, offset) 852 | offset += sizeof(off_t) 853 | result.append(a.value) 854 | 855 | # Extended common attributes 856 | if attrs[4] & ATTR_CMNEXT_RELPATH: 857 | a = attrreference_t.from_buffer(buf, offset) 858 | ofs = offset + a.attr_dataoffset 859 | path = _decode_utf8_nul(buf[ofs:ofs+a.attr_length]) 860 | offset += sizeof (attrreference_t) 861 | result.append(path) 862 | if attrs[4] & ATTR_CMNEXT_PRIVATESIZE: 863 | a = off_t.from_buffer(buf, offset) 864 | offset += sizeof(off_t) 865 | result.append(a.value) 866 | if attrs[4] & ATTR_CMNEXT_LINKID: 867 | a = uint64_t.from_buffer(buf, offset) 868 | offset += sizeof(uint64_t) 869 | result.append(a.value) 870 | if attrs[4] & ATTR_CMNEXT_NOFIRMLINKPATH: 871 | a = attrreference_t.from_buffer(buf, offset) 872 | ofs = offset + a.attr_dataoffset 873 | path = _decode_utf8_nul(buf[ofs:ofs+a.attr_length]) 874 | offset += sizeof (attrreference_t) 875 | result.append(path) 876 | if attrs[4] & ATTR_CMNEXT_REALDEVID: 877 | a = dev_t.from_buffer(buf, offset) 878 | offset += sizeof(dev_t) 879 | result.append(a.value) 880 | if attrs[4] & ATTR_CMNEXT_REALFSID: 881 | a = fsid_t.from_buffer(buf, offset) 882 | offset += sizeof(fsid_t) 883 | result.append(a.value) 884 | if attrs[4] & ATTR_CMNEXT_CLONEID: 885 | a = uint64_t.from_buffer(buf, offset) 886 | offset += sizeof(uint64_t) 887 | result.append(a.value) 888 | if attrs[4] & ATTR_CMNEXT_EXT_FLAGS: 889 | a = uint64_t.from_buffer(buf, offset) 890 | offset += sizeof(uint64_t) 891 | result.append(a.value) 892 | 893 | return result 894 | 895 | # Sadly, ctypes.get_errno() seems not to work 896 | __error = libc.__error 897 | __error.restype = POINTER(c_int) 898 | 899 | def _get_errno(): 900 | return __error().contents.value 901 | 902 | def getattrlist(path, attrs, options): 903 | if not isinstance(path, bytes): 904 | path = path.encode('utf-8') 905 | attrs = list(attrs) 906 | if attrs[1]: 907 | attrs[1] |= ATTR_VOL_INFO 908 | alist = attrlist(bitmapcount=5, 909 | commonattr=attrs[0], 910 | volattr=attrs[1], 911 | dirattr=attrs[2], 912 | fileattr=attrs[3], 913 | forkattr=attrs[4]) 914 | 915 | bufsize = _attrbuf_size(attrs) 916 | buf = create_string_buffer(bufsize) 917 | 918 | ret = _getattrlist(path, byref(alist), buf, bufsize, 919 | options | FSOPT_REPORT_FULLSIZE) 920 | 921 | if ret < 0: 922 | err = _get_errno() 923 | raise OSError(err, os.strerror(err), path) 924 | 925 | return _decode_attrlist_result(buf, attrs, options) 926 | 927 | def fgetattrlist(fd, attrs, options): 928 | if hasattr(fd, 'fileno'): 929 | fd = fd.fileno() 930 | attrs = list(attrs) 931 | if attrs[1]: 932 | attrs[1] |= ATTR_VOL_INFO 933 | alist = attrlist(bitmapcount=5, 934 | commonattr=attrs[0], 935 | volattr=attrs[1], 936 | dirattr=attrs[2], 937 | fileattr=attrs[3], 938 | forkattr=attrs[4]) 939 | 940 | bufsize = _attrbuf_size(attrs) 941 | buf = create_string_buffer(bufsize) 942 | 943 | ret = _fgetattrlist(fd, byref(alist), buf, bufsize, 944 | options | FSOPT_REPORT_FULLSIZE) 945 | 946 | if ret < 0: 947 | err = _get_errno() 948 | raise OSError(err, os.strerror(err)) 949 | 950 | return _decode_attrlist_result(buf, attrs, options) 951 | 952 | def statfs(path): 953 | if not isinstance(path, bytes): 954 | path = path.encode('utf-8') 955 | result = struct_statfs() 956 | ret = _statfs(path, byref(result)) 957 | if ret < 0: 958 | err = _get_errno() 959 | raise OSError(err, os.strerror(err), path) 960 | return result 961 | 962 | def fstatfs(fd): 963 | if hasattr(fd, 'fileno'): 964 | fd = fd.fileno() 965 | result = struct_statfs() 966 | ret = _fstatfs(fd, byref(result)) 967 | if ret < 0: 968 | err = _get_errno() 969 | raise OSError(err, os.strerror(err)) 970 | return result 971 | -------------------------------------------------------------------------------- /mac_alias/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import datetime 5 | 6 | ZERO = datetime.timedelta(0) 7 | class UTC (datetime.tzinfo): 8 | def utcoffset(self, dt): 9 | return ZERO 10 | def dst(self, dt): 11 | return ZERO 12 | def tzname(self, dt): 13 | return 'UTC' 14 | 15 | utc = UTC() 16 | mac_epoch = datetime.datetime(1904,1,1,0,0,0,0,utc) 17 | unix_epoch = datetime.datetime(1970,1,1,0,0,0,0,utc) 18 | osx_epoch = datetime.datetime(2001,1,1,0,0,0,0,utc) 19 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | # ~/Library/Application Support/com.apple.sharedfilelist/ 5 | # https://www.mac4n6.com/blog/2017/10/17/script-update-for-macmrupy-v13-new-1013-sfl2-mru-files 6 | 7 | import os 8 | import re 9 | import sys 10 | import json 11 | import time 12 | import pinyin 13 | import plistlib 14 | import ccl_bplist 15 | from mac_alias import Bookmark 16 | try: 17 | from urllib import unquote # Python 2.X 18 | except ImportError: 19 | from urllib.parse import unquote # Python 3+ 20 | 21 | 22 | def BLOBParser_human(blob): 23 | # http://mac-alias.readthedocs.io/en/latest/bookmark_fmt.html 24 | try: 25 | b = Bookmark.from_bytes(blob) 26 | return "/" + u"/".join(b.get(0x1004, default=None)) 27 | except Exception as e: 28 | print(e) 29 | 30 | 31 | # for 10.13 32 | def ParseSFL2(MRUFile): 33 | itemsLinkList = [] 34 | try: 35 | with open(MRUFile, "rb") as plistfile: 36 | plist = ccl_bplist.load(plistfile) 37 | plist_objects = ccl_bplist.deserialise_NsKeyedArchiver( 38 | plist, parse_whole_structure=True) 39 | if plist_objects["root"]["NS.keys"][0] == "items": 40 | for item in plist_objects["root"]["NS.objects"][0]["NS.objects"]: 41 | attributes = dict(zip(item["NS.keys"], item["NS.objects"])) 42 | # print(attributes["Bookmark"].decode("iso-8859-1")) 43 | if "Bookmark" in attributes: 44 | if isinstance(attributes["Bookmark"], str): 45 | itemLink = BLOBParser_human(attributes["Bookmark"]) 46 | elif isinstance(attributes["Bookmark"], bytes): 47 | itemLink = BLOBParser_human(attributes["Bookmark"]) 48 | else: 49 | itemLink = BLOBParser_human(attributes["Bookmark"]['NS.data']) 50 | # print(itemLink) 51 | itemsLinkList.append(itemLink) 52 | return itemsLinkList 53 | except Exception as e: 54 | print("#ERROR:", e) 55 | 56 | 57 | # for 10.11, 10.12 58 | def ParseSFL(MRUFile): 59 | itemsLinkList = [] 60 | try: 61 | with open(MRUFile, "rb") as plistfile: 62 | plist = ccl_bplist.load(plistfile) 63 | plist_objects = ccl_bplist.deserialise_NsKeyedArchiver( 64 | plist, parse_whole_structure=True) 65 | if plist_objects["root"]["NS.keys"][2] == "items": 66 | items = plist_objects["root"]["NS.objects"][2]["NS.objects"] 67 | for n, item in enumerate(items): 68 | # item["URL"]['NS.relative'] file:///xxx/xxx/xxx 69 | filePath = item["URL"]['NS.relative'][7:] 70 | # /xxx/xxx/xxx/ the last "/" make basename func not work 71 | if filePath[-1] == '/': 72 | filePath = filePath[:-1] 73 | itemsLinkList.append(filePath) 74 | return itemsLinkList 75 | except Exception as e: 76 | print(e) 77 | 78 | 79 | # for Finder 80 | def ParseFinderPlist(MRUFile): 81 | itemsLinkList = [] 82 | try: 83 | with open(MRUFile, "rb") as plistfile: 84 | plist = ccl_bplist.load(plistfile) 85 | for item in plist["FXRecentFolders"]: 86 | if "file-bookmark" in item: 87 | blob = item["file-bookmark"] 88 | elif "file-data" in item: 89 | blob = item["file-data"]["_CFURLAliasData"] 90 | itemLink = BLOBParser_human(blob) 91 | # exclude / path 92 | if itemLink == "/": 93 | continue 94 | itemsLinkList.append(itemLink) 95 | return itemsLinkList 96 | except Exception as e: 97 | print(e) 98 | 99 | 100 | # for Sublime Text 3 101 | def ParseSublimeText3Session(sessionFile): 102 | with open(sys.argv[1]) as ff: 103 | jsonObj = json.load(ff) 104 | itemsLinkList = jsonObj["settings"]["new_window_settings"]["file_history"][0:15] 105 | return itemsLinkList 106 | 107 | 108 | # deprecated 109 | def ParseMSOffice2016Plist(MRUFile): 110 | itemsLinkList = [] 111 | try: 112 | plistfile = plistlib.readPlist(MRUFile) 113 | for item in plistfile: 114 | itemsLinkList.append(unquote(item[7:]).decode('utf8')) 115 | return itemsLinkList 116 | except Exception as e: 117 | # When read binary property list 118 | # xml.parsers.expat.ExpatError: not well-formed (invalid token) 119 | print(e) 120 | 121 | 122 | # deprecated 123 | def ParseMSOffice2019Plist(MRUFile): 124 | itemsLinkList = [] 125 | try: 126 | with open(MRUFile, "rb") as plistfile: 127 | plist = ccl_bplist.load(plistfile) 128 | itemsLinkList = [unquote(item[7:].encode("utf8")) for item in plist.keys()] 129 | return itemsLinkList 130 | except Exception as e: 131 | # When read xml property list 132 | # ccl_bplist.BplistError: Bad file header 133 | print(e) 134 | 135 | 136 | def ParseMSOfficePlist(MRUFile): 137 | itemsLinkList = [] 138 | try: 139 | # office 2019 140 | with open(MRUFile, "rb") as plistfile: 141 | plist = ccl_bplist.load(plistfile) 142 | itemsLinkList = [unquote(item[7:].encode("utf8")).decode('utf8') for item in plist.keys()] 143 | return itemsLinkList 144 | except ccl_bplist.BplistError as e: 145 | # office 2016 146 | plistfile = plistlib.readPlist(MRUFile) 147 | for item in plistfile: 148 | itemsLinkList.append(unquote(item[7:]).decode('utf8')) 149 | return itemsLinkList 150 | except Exception as e: 151 | print(e) 152 | 153 | 154 | def checkFileList(fileList): 155 | newFileList = [] 156 | excludedFolderList = os.environ["ExcludedFolders"].split(":") if os.environ["ExcludedFolders"] else [] 157 | excludedFilesList = os.environ["ExcludedFiles"].split(":") if os.environ["ExcludedFiles"] else [] 158 | for filePath in fileList: 159 | if not os.path.exists(filePath): 160 | continue 161 | fileExclude = False 162 | for exFilePath in excludedFilesList: 163 | exFilePath = os.path.expanduser(exFilePath) 164 | # 检测是否为文件路径 165 | if not os.path.isfile(exFilePath): 166 | break 167 | if os.path.samefile(filePath, exFilePath): 168 | fileExclude = True 169 | break 170 | for exFolderPath in excludedFolderList: 171 | exFolderPath = os.path.expanduser(exFolderPath) 172 | # 检测是否为文件夹路径 173 | if not os.path.isdir(exFolderPath): 174 | continue 175 | # change "/xxx/xx" to "/xxx/xx/" 176 | # for distinguish "/xxx/xx" and "/xxx/xx x/xx" 177 | exFolderPath = os.path.join(exFolderPath, "") 178 | # change "/xxx/xx" to "/xxx/xx/" for comparing exFolderPath "/xxx/xx/" and filePath "/xxx/xx" 179 | fileFullPath = (filePath + "/") if os.path.isdir(filePath) else filePath 180 | if fileFullPath.startswith(exFolderPath): 181 | fileExclude = True 182 | break 183 | if not fileExclude: 184 | newFileList.append(filePath) 185 | return newFileList 186 | 187 | 188 | # convert "abc你好def" to "abc ni hao def" 189 | def convert2Pinyin(filename): 190 | # convert "你好" to " ni hao " 191 | def c2p(matchObj): 192 | return " " + pinyin.get(matchObj.group(), format="strip", delimiter=" ") + " " 193 | # replace chinese character with pinyin 194 | return re.sub(u'[\u4e00-\u9fff]+', c2p, filename) 195 | 196 | 197 | if __name__ == '__main__': 198 | allItemsLinkList = [] 199 | for filePath in sys.argv[1:]: 200 | if filePath.endswith(".sfl2"): 201 | if __debug__: print("#FileType: sfl2") # noqa 202 | itemsLinkList = ParseSFL2(filePath) 203 | if filePath.endswith(".sfl3"): 204 | if __debug__: print("#FileType: sfl3") # noqa 205 | itemsLinkList = ParseSFL2(filePath) 206 | elif filePath.endswith(".sfl"): 207 | if __debug__: print("#FileType: sfl") # noqa 208 | itemsLinkList = ParseSFL(filePath) 209 | elif filePath.endswith("com.apple.finder.plist"): 210 | if __debug__: print("#FileType: com.apple.finder.plist") # noqa 211 | itemsLinkList = ParseFinderPlist(filePath) 212 | elif filePath.endswith(".securebookmarks.plist"): 213 | if __debug__: print("#FileType: .securebookmarks.plist") # noqa 214 | itemsLinkList = ParseMSOfficePlist(filePath) 215 | elif filePath.endswith(".sublime_session"): 216 | if __debug__: print("#FileType: sublime_session") # noqa 217 | itemsLinkList = ParseSublimeText3Session(filePath) 218 | allItemsLinkList.extend(itemsLinkList) 219 | allItemsLinkList = checkFileList(allItemsLinkList) 220 | 221 | # use current app to open recent documents of current app 222 | currentAppPath = os.getenv("currentAppPath") 223 | if currentAppPath: 224 | result = {"variables": {"currentAppPath": currentAppPath}, "items": []} 225 | else: 226 | result = {"items": []} 227 | 228 | for n, item in enumerate(allItemsLinkList): 229 | # remove records of file not exist 230 | if not os.path.exists(item): 231 | continue 232 | modifiedTimeSecNum = os.path.getmtime(item) 233 | modifiedTime = time.strftime("%m-%d %H:%M", time.localtime(modifiedTimeSecNum)) 234 | filename = os.path.basename(item) 235 | temp = { 236 | "type": "file", 237 | "title": filename, 238 | "autocomplete": filename, 239 | # replace "." with space for searching filename extension 240 | "match": filename.replace('.', ' ') + " " + convert2Pinyin(filename), 241 | "icon": {"type": "fileicon", "path": item}, 242 | "subtitle": u"❖" + modifiedTime + u" ❥" + item.replace(os.environ["HOME"], "~"), 243 | "arg": item 244 | } 245 | result['items'].append(temp) 246 | if result['items']: 247 | print(json.dumps(result)) 248 | else: 249 | # when result list is empty 250 | print('{"items": [{"title": "None Recent Record","subtitle": "(*´・д・)?"}]}') 251 | -------------------------------------------------------------------------------- /pinyin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .pinyin import * 4 | -------------------------------------------------------------------------------- /pinyin/_compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | py2 = sys.version_info < (3, 0) 3 | 4 | 5 | if py2: 6 | str_type = unicode 7 | 8 | def u(s): 9 | if not isinstance(s, unicode): 10 | s = unicode(s, "utf-8") 11 | return s 12 | 13 | else: 14 | str_type = str 15 | 16 | def u(s): 17 | return s 18 | -------------------------------------------------------------------------------- /pinyin/cmd.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from pinyin import get 3 | 4 | from pinyin._compat import u 5 | 6 | 7 | def pinyin(): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("chars", help="Input chinese words") 10 | args = parser.parse_args() 11 | 12 | if not args.chars: 13 | parser.print_help() 14 | return 15 | 16 | print(get(u(args.chars))) 17 | -------------------------------------------------------------------------------- /pinyin/pinyin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import itertools 5 | import unicodedata 6 | 7 | from ._compat import u 8 | 9 | __all__ = ['get', 'get_pinyin', 'get_initial'] 10 | 11 | 12 | # init pinyin dict 13 | pinyin_dict = {} 14 | pinyin_tone = {} 15 | dat = os.path.join(os.path.dirname(__file__), "Mandarin.dat") 16 | with open(dat) as f: 17 | for line in f: 18 | k, v = line.strip().split('\t') 19 | pinyin_dict[k] = u(v.lower().split(" ")[0][:-1]) 20 | pinyin_tone[k] = int(v.lower().split(" ")[0][-1]) 21 | 22 | 23 | def _pinyin_generator(chars, format): 24 | """Generate pinyin for chars, if char is not chinese character, 25 | itself will be returned. 26 | Chars must be unicode list. 27 | """ 28 | for char in chars: 29 | key = "%X" % ord(char) 30 | pinyin = pinyin_dict.get(key, char) 31 | tone = pinyin_tone.get(key, 0) 32 | 33 | if tone == 0 or format == "strip": 34 | pass 35 | elif format == "numerical": 36 | pinyin += str(tone) 37 | elif format == "diacritical": 38 | # Find first vowel -- where we should put the diacritical mark 39 | vowels = itertools.chain((c for c in pinyin if c in "aeo"), 40 | (c for c in pinyin if c in "iuv")) 41 | vowel = pinyin.index(next(vowels)) + 1 42 | pinyin = pinyin[:vowel] + tonemarks[tone] + pinyin[vowel:] 43 | else: 44 | error = "Format must be one of: numerical/diacritical/strip" 45 | raise ValueError(error) 46 | 47 | yield unicodedata.normalize('NFC', pinyin) 48 | 49 | 50 | def get(s, delimiter='', format="diacritical"): 51 | """Return pinyin of string, the string must be unicode 52 | """ 53 | return delimiter.join(_pinyin_generator(u(s), format=format)) 54 | 55 | 56 | def get_pinyin(s): 57 | """This function is only for backward compatibility, use `get` instead. 58 | """ 59 | import warnings 60 | warnings.warn('Deprecated, use `get` instead.') 61 | return get(s) 62 | 63 | 64 | def get_initial(s, delimiter=' '): 65 | """Return the 1st char of pinyin of string, the string must be unicode 66 | """ 67 | initials = (p[0] for p in _pinyin_generator(u(s), format="strip")) 68 | return delimiter.join(initials) 69 | 70 | tonemarks = ["", u("̄"), u("́"), u("̌"), u("̀"), ""] 71 | --------------------------------------------------------------------------------