-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
294 lines (247 loc) · 10 KB
/
__init__.py
File metadata and controls
294 lines (247 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# -*- coding: utf-8 -*-
from pathlib import Path, PurePath
from typing import List, Optional, Generator, Callable
import fnmatch
import os
import sys
from albert import *
try:
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
except ImportError as e:
critical(f"Failed to import PyGObject: {e}")
critical(f"Python version: {sys.version}")
critical(f"Python executable: {sys.executable}")
critical(f"Python path: {sys.path}")
raise ImportError("PyGObject (gi) is required. Install it with: pip install PyGObject")
md_iid = "5.0"
md_version = "0.0.1"
md_name = "Recent-Files"
md_description = "Access recent files"
md_license = "MIT"
md_url = "https://github.com/ffpyt/albert-plugin-python-recent-files"
md_readme_url = "https://github.com/ffpyt/albert-plugin-python-recent-files/blob/master/README.md"
md_authors = ["@ffpyt"]
md_maintainers = ["@ffpyt"]
md_credits = ["@cardoprimo"]
md_platforms = ["Linux"]
FILTER_MODIFIER_TO_MIME_TYPE = {
"d ": "inode/directory",
"v ": "video/*",
"a ": "audio/*",
"i ": "image/*",
"f ": "text/*",
}
def is_path_excluded(path: PurePath, excluded_patterns: List[str]) -> bool:
"""Check if path matches any of the excluded patterns."""
path_str = str(path)
for pattern in excluded_patterns:
# Convert glob pattern to regex and match against full path
if fnmatch.fnmatch(path_str, pattern) or \
fnmatch.fnmatch(path_str, f"{pattern}/*") or \
any(fnmatch.fnmatch(str(p), pattern) for p in path.parents):
return True
return False
def search_recent_files(search_term: str = "", mime_type: Optional[str] = None) -> List:
"""
Search recent files using Gtk.RecentManager.
Filters out non-existing files and folders.
:param search_term: Search term to filter files by URI
:param mime_type: MIME type filter (supports wildcards like 'image/*')
:return: List of recent file items sorted by visit date
"""
recent_manager = Gtk.RecentManager.get_default()
recent_files = recent_manager.get_items() # type: List[Gtk.RecentItem]
if not recent_files:
return []
# Filter by MIME type if specified
if mime_type is not None:
type_part, _, subtype_part = mime_type.partition('/')
filtered_files = []
for file in recent_files:
# e.g. "image/png", "application/pdf", "text/plain""
file_mime = file.get_mime_type()
if file_mime:
# Exact match or wildcard match
if mime_type == file_mime or (subtype_part == '*' and file_mime.startswith(f"{type_part}/")):
filtered_files.append(file)
recent_files = filtered_files
# Filter by search term
if search_term:
search_lower = search_term.lower()
recent_files = [f for f in recent_files if search_lower in f.get_uri().lower()]
recent_files = [
f
for f in recent_files
# remove non-existing files and folders
if Path(f.get_uri_display()).exists()
]
# Sort by most recently visited
return sorted(recent_files, key=lambda x: x.get_visited(), reverse=True)
class Plugin(PluginInstance, GeneratorQueryHandler):
"""Albert plugin to access recently used files"""
def __init__(self):
PluginInstance.__init__(self)
GeneratorQueryHandler.__init__(self)
# Exclusion patterns configuration
self._exclude_patterns = self.readConfig('exclude_patterns', str)
if self._exclude_patterns is None:
self._exclude_patterns = []
self.writeConfig('exclude_patterns', "")
# Result limit configuration
self._limit = self.readConfig('limit', int)
if self._limit is None:
self._limit = 50
self.writeConfig('limit', 50)
def defaultTrigger(self):
return "rf "
def synopsis(self, query: str) -> str:
"""
Returns the input hint for the given **query**.
The returned string will be displayed in the input line if space permits.
"""
recent_files = self.query_recent_files(query)
if len(recent_files) > 0:
return f"Found {len(recent_files)} items."
return ""
@property
def exclude_patterns(self):
return self._exclude_patterns
@exclude_patterns.setter
def exclude_patterns(self, value):
excluded_patterns = []
if value not in [None, ""]:
excluded_patterns = [
pattern.strip()
for pattern in value.split(",")
if pattern.strip() != ""
]
# Expand user home directory in patterns
excluded_patterns = [
os.path.expanduser(pattern)
for pattern in excluded_patterns
]
self._exclude_patterns = excluded_patterns
self.writeConfig('exclude_patterns', value)
@property
def limit(self):
return self._limit
@limit.setter
def limit(self, value):
self._limit = value
self.writeConfig('limit', value)
def items(self, context: QueryContext) -> Generator[List[Item]]:
"""Generate items for the query"""
items = []
try:
# Parse filter modifiers from query
query = context.query
recent_files = self.query_recent_files(query)
if not recent_files:
items.append(
StandardItem(
id=self.id(),
text="No recent files found",
subtext="Try a different search or filter",
icon_factory=lambda: Icon.theme("dialog-information"),
)
)
yield items
return
# Filter and limit results
for recent_file in recent_files:
if len(items) >= self.limit:
break
uri_display = recent_file.get_uri_display()
file_path = Path(uri_display)
display_name = recent_file.get_display_name()
mime_type = recent_file.get_mime_type()
# Check if path should be excluded
if is_path_excluded(PurePath(file_path), self.exclude_patterns):
continue
def make_icon_factory(path: str | Path):
path = Path(path)
if path.is_dir():
return lambda: Icon.standard(Icon.StandardIconType.DirIcon)
elif path.is_file():
return lambda: Icon.fileType(str(path))
else:
return lambda: Icon.standard(Icon.StandardIconType.MessageBoxQuestion)
# Create action callbacks with proper closure
def make_open_action(p):
return lambda: openUrl(f"file://{p}")
def make_copy_action(p):
return lambda: setClipboardText(p)
def make_reveal_action(p):
return lambda: runDetachedProcess(["xdg-open", str(Path(p).parent)])
# Create item
items.append(
StandardItem(
id=uri_display,
text=display_name if display_name else file_path.name,
subtext=f"{file_path} ({mime_type})" if mime_type else file_path,
icon_factory=make_icon_factory(file_path),
actions=[
Action("open", "Open with default application", make_open_action(str(file_path))),
Action("copy", "Copy path to clipboard", make_copy_action(str(file_path))),
Action("reveal", "Reveal in file browser", make_reveal_action(file_path)),
],
)
)
except FileNotFoundError as e:
items.append(
StandardItem(
id=self.id(),
text="No recent files found",
subtext=str(e),
icon_factory=lambda: Icon.theme("dialog-warning"),
)
)
except Exception as e:
critical(f"Error accessing recent files: {str(e)}")
items.append(
StandardItem(
id=self.id(),
text="Error accessing recent files",
subtext=str(e),
icon_factory=lambda: Icon.theme("dialog-error"),
)
)
yield items
def query_recent_files(self, query: str) -> list[Gtk.RecentItem]:
"""
Runs a query against the recent files manager.
Returns an already sorted and filtered list of recent files.
ATTENTION: Has not yet filtered non-existing files.
"""
query = query.lstrip()
# Check for filter modifiers at the start of query
first_letters = query[:2]
mime_filter = FILTER_MODIFIER_TO_MIME_TYPE.get(first_letters, None)
if mime_filter is not None:
query = query[2:].strip()
recent_files = search_recent_files(search_term=query, mime_type=mime_filter)
return recent_files
def configWidget(self):
return [
{
"type": "lineedit",
"property": "exclude_patterns",
"label": "Glob Pattern Exclusion",
"widget_properties": {
"toolTip": "Comma separated list of glob patterns to exclude from the search, e.g. */tmp, ~/dev, *.txt",
"placeholderText": "e.g., */tmp, ~/dev, *.txt"
},
},
{
"type": "spinbox",
"property": "limit",
"label": "Maximum number of results",
"widget_properties": {
"toolTip": "Maximum number of recent files to display",
"minimum": 1,
"maximum": 200,
},
},
]