App Gallery#
Random Password Generator#
Difficulty: Easy
The user enters the length of the password, and the UI generates a few random passwords for the user to choose from. The user can tap “Enter” to copy the selected password to the clipboard.
Demo
Source Code
1# -*- coding: utf-8 -*-
2
3"""
4Feature:
5
6The user enters the length of the password, and the UI generates a few random passwords
7for the user to choose from. The user can tap "Enter" to copy the selected password
8to the clipboard, and the UI will exit.
9
10Difficulty: Easy
11
12Dependencies:
13
14.. code-block:: bash
15
16 pip install pyperclip>=1.8.0,<2.0.0
17
18Demo: https://asciinema.org/a/617869
19"""
20
21import typing as T
22import string
23import random
24import dataclasses
25
26# we need this library to copy text to clipboard
27import pyperclip
28
29# import zelfred public API
30import zelfred.api as zf
31
32
33# implement the random password generator
34def remove_chars(chars: str, to_remove: str) -> str:
35 for c in to_remove:
36 chars = chars.replace(c, "")
37 return chars
38
39
40to_remove = "iIlLoO1O" # we don't use these chars because they are easily confused
41
42charset_lower = remove_chars(string.ascii_lowercase, to_remove)
43charset_upper = remove_chars(string.ascii_uppercase, to_remove)
44charset_digits = remove_chars(string.digits, to_remove)
45charset_symbol = "!@#$%^&*()_+"
46charset = charset_lower + charset_upper + charset_digits + charset_symbol
47
48
49def random_password(length: int) -> str:
50 # first char must be a letter
51 first = random.choice(charset_lower + charset_upper)
52 # must have at least 1-2 digits, 1-2 uppercase, 1-2 lowercase, 1-2 symbol
53 digits = random.choices(charset_digits, k=random.randint(1, 2))
54 upper = random.choices(charset_upper, k=random.randint(1, 2))
55 lower = random.choices(charset_lower, k=random.randint(1, 2))
56 symbol = random.choices(charset_symbol, k=random.randint(1, 2))
57 # the rest can be anything
58 k = length - 1 - len(digits) - len(upper) - len(lower) - len(symbol)
59 if k:
60 rest = random.choices(charset, k=k)
61 else:
62 rest = []
63 tail = digits + upper + lower + symbol + rest
64 random.shuffle(tail)
65 return first + "".join(tail)
66
67
68@dataclasses.dataclass
69class Item(zf.Item):
70 """
71 The default ``zf.Item`` does not implement any user action handler methods.
72 To copy the password to the clipboard when the user taps "Enter," we need to
73 define a custom item class and override the ``enter_handler`` method."
74
75 By default, the UI can perform a "user action" when the following keys are tapped:
76
77 - Enter
78 - Ctrl A
79 - Ctrl W
80 - Ctrl U
81 - Ctrl P
82
83 When user taps one of these keys, the UI will call the corresponding handler method
84 and exit immediately.
85 """
86
87 def enter_handler(self, ui: zf.UI):
88 """
89 Copy the content to clipboard.
90 """
91 pyperclip.copy(self.arg)
92
93
94def handler(query: str, ui: zf.UI) -> T.List[Item]:
95 """
96 The handler is the core of a Zelfred App. It's a user-defined function
97 that takes the entered query and the UI object as inputs and returns
98 a list of items to render.
99 """
100 # if query is empty, display helper text
101 if bool(query) is False:
102 return [
103 Item(
104 title="Enter the length of the password ...",
105 subtitle="for example: 12",
106 # user can tap "Tab" to autocomplete the query
107 # we would like to generate a password with length 12 in most of the case
108 # so we set the autocomplete to 12
109 autocomplete="12",
110 )
111 ]
112 else:
113 # try to convert the query into an integer
114 try:
115 length = int(query)
116 # if the length is less than 10, display helper text
117 if length < 10:
118 return [
119 Item(
120 title=f"We don't support password length less than 10.",
121 subtitle="please enter a number greater than 10!",
122 )
123 ]
124 # if the length is valid, then generate some passwords
125 else:
126 items = list()
127 for _ in range(20):
128 pwd = random_password(length)
129 item = Item(
130 # the first line of the item in the UI, usually the content of the item.
131 # the ui.terminal is a blessed.Terminal object,
132 # we can use it to add syntax highlight to the text.
133 # you can find more information at https://blessed.readthedocs.io/en/latest/colors.html
134 title=f"{ui.terminal.cyan}{pwd}{ui.terminal.normal}",
135 # the second line of the item in the UI, usually some helper text.
136 subtitle=f"tap {ui.terminal.magenta}Enter{ui.terminal.normal} to copy this password to clipboard",
137 # the argument of the item will be used to copy to clipboard
138 arg=pwd,
139 )
140 items.append(item)
141 return items
142 # if the query is not a valid integer, display helper text
143 except ValueError:
144 return [
145 Item(
146 title=f"{query!r} is not a valid length.",
147 subtitle="please enter an integer!",
148 )
149 ]
150
151
152if __name__ == "__main__":
153 # reset the debugger and enable it
154 zf.debugger.reset()
155 zf.debugger.enable()
156
157 # create the UI and run it
158 ui = zf.UI(
159 # tell the UI to use the handler function
160 handler=handler,
161 # capture error and display debug information in the UI,
162 # this is the default behavior, if this is set to False, then it will
163 # raise Exception and stop the program.
164 capture_error=True,
165 )
166 # run the UI
167 ui.run()
Calculate File Checksum#
Difficulty: Easy
The user enters (or paste) the absolute path of the file, and the UI generates a few checksum algorithm options, then user can choose one and hit “Enter” to copy the checksum value of the selected algorithm. The UI will stay and user can continue to choose another algorithm and hit “Enter” again.
Demo
Source Code
1# -*- coding: utf-8 -*-
2
3"""
4Feature:
5
6The user enters (or paste) the absolute path of the file, and the UI generates
7a few checksum algorithm options, then user can choose one and hit "Enter" to
8copy the checksum value of the selected algorithm. The UI will stay and user can
9continue to choose another algorithm and hit "Enter" again.
10
11Difficulty: Easy
12
13Dependencies:
14
15.. code-block:: bash
16
17 pip install pyperclip>=1.8.0,<2.0.0
18
19Demo: https://asciinema.org/a/617871
20"""
21
22import typing as T
23import hashlib
24import dataclasses
25from pathlib import Path
26
27# we need this library to copy text to clipboard
28import pyperclip
29
30# import zelfred public API
31import zelfred.api as zf
32
33
34# implement the checksum calculator
35def md5_of_file(path: Path) -> str:
36 return hashlib.md5(path.read_bytes()).hexdigest()
37
38
39def sha1_of_file(path: Path) -> str:
40 return hashlib.sha1(path.read_bytes()).hexdigest()
41
42
43def sha256_of_file(path: Path) -> str:
44 return hashlib.sha256(path.read_bytes()).hexdigest()
45
46
47def sha512_of_file(path: Path) -> str:
48 return hashlib.sha512(path.read_bytes()).hexdigest()
49
50
51algorithms = [
52 "md5",
53 "sha1",
54 "sha256",
55 "sha512",
56]
57
58algorithm_to_function_mapper = {
59 "md5": md5_of_file,
60 "sha1": sha1_of_file,
61 "sha256": sha256_of_file,
62 "sha512": sha512_of_file,
63}
64
65
66@dataclasses.dataclass
67class Item(zf.Item):
68 """
69 The default ``zf.Item`` does not implement any user action handler methods.
70 To copy the password to the clipboard when the user taps "Enter," we need to
71 define a custom item class and override the ``enter_handler`` method."
72
73 By default, the UI can perform a "user action" when the following keys are tapped:
74
75 - Enter
76 - Ctrl A
77 - Ctrl W
78 - Ctrl U
79 - Ctrl P
80
81 When the user taps one of these keys, the UI calls the corresponding
82 handler method and exits immediately. To modify this behavior, we can
83 override the ``post_enter_handler`` method, which is invoked after the
84 ``enter_handler`` method."
85 """
86
87 def enter_handler(self, ui: zf.UI):
88 """
89 Copy the checksum of the file to clipboard.
90
91 The path of the file is stored in the ``self.variables["path"]``.
92 The checksum algorithm is stored in the ``self.variables["algo"]``.
93 """
94 path = Path(self.variables["path"])
95 algo = self.variables["algo"]
96 checksum = algorithm_to_function_mapper[algo](path)
97 pyperclip.copy(checksum)
98
99 def post_enter_handler(self, ui: zf.UI):
100 """
101 We would like to keep the UI displayed after the user hits 'Enter'.
102 Typically, any user input change will trigger the UI to re-render
103 and then wait for the next user input. The 'UI.wait_next_user_input()'
104 method allows you to skip the re-rendering and wait for the next user input."
105 """
106 ui.wait_next_user_input()
107
108
109def handler(query: str, ui: zf.UI) -> T.List[Item]:
110 """
111 The handler is the core of a Zelfred App. It's a user-defined function
112 that takes the entered query and the UI object as inputs and returns
113 a list of items to render.
114 """
115 # if query is empty, display helper text
116 if bool(query) is False:
117 return [
118 Item(
119 title="Enter the absolute path of the file ...",
120 subtitle="for example: ${HOME}/.zshrc",
121 # since my demo is running on macOS, I use the ${HOME}/.zshrc file
122 # as an example, you can change it to any file you want.
123 autocomplete=str(Path.home().joinpath(".zshrc")),
124 )
125 ]
126 else:
127 path = Path(query)
128 # if the path does not exist, user might be still typing,
129 # so we just display a helper text
130 if path.exists() is False:
131 return [
132 Item(
133 title="Keep entering ...",
134 subtitle=f"{path} doesn't exists.",
135 )
136 ]
137 # if the path is a directory, we display a helper text
138 elif path.is_dir():
139 return [
140 Item(
141 title="🔴 We cannot calculate checksum for a directory!",
142 subtitle=f"{path} is a directory.",
143 )
144 ]
145 # if the path is a file, we display the checksum options
146 elif path.is_file():
147 return [
148 Item(
149 title=f"{algo} of {path}",
150 # the ui.terminal is a blessed.Terminal object,
151 # we can use it to add syntax highlight to the text.
152 # you can find more information at https://blessed.readthedocs.io/en/latest/colors.html
153 subtitle=f"tap {ui.terminal.magenta}Enter{ui.terminal.normal} to copy this checksum to clipboard.",
154 variables={
155 "path": f"{path}",
156 "algo": algo,
157 },
158 )
159 for algo in algorithms
160 ]
161 else: # this should never happen, but just in case
162 raise NotImplementedError
163
164
165if __name__ == "__main__":
166 # reset the debugger and enable it
167 zf.debugger.reset()
168 zf.debugger.enable()
169
170 # create the UI and run it
171 ui = zf.UI(
172 # tell the UI to use the handler function
173 handler=handler,
174 # capture error and display debug information in the UI,
175 # this is the default behavior, if this is set to False, then it will
176 # raise Exception and stop the program.
177 capture_error=True,
178 )
179 # run the UI
180 ui.run()
Select Item Using Fuzzy Match#
Difficulty: Easy
Use the user input to sort a list of items by fuzzy match similarity. Allow user to tap “Enter” to copy the content to clipboard.
Demo
Source Code
1# -*- coding: utf-8 -*-
2
3"""
4Feature:
5
6Use the user input to sort a list of items by fuzzy match similarity.
7Allow user to tap "Enter" to copy the content to clipboard.
8
9Dependencies:
10
11.. code-block:: bash
12
13 pip install fuzzywuzzy>=0.18.0,<1.0.0
14 pip install python-Levenshtein>=0.21.0,<1.0.0
15 pip install pyperclip>=1.8.0,<2.0.0
16
17Demo: https://asciinema.org/a/617874
18"""
19
20import dataclasses
21
22# we need this library to copy text to clipboard
23import pyperclip
24
25# we need this library to do fuzzy match
26from fuzzywuzzy import process
27
28# import zelfred public API
29import zelfred.api as zf
30
31
32@dataclasses.dataclass
33class Item(zf.Item):
34 """
35 The default ``zf.Item`` does not implement any user action handler methods.
36 To copy the password to the clipboard when the user taps "Enter," we need to
37 define a custom item class and override the ``enter_handler`` method."
38
39 By default, the UI can perform a "user action" when the following keys are tapped:
40
41 - Enter
42 - Ctrl A
43 - Ctrl W
44 - Ctrl U
45 - Ctrl P
46
47 When user taps one of these keys, the UI will call the corresponding handler method
48 and exit immediately.
49 """
50
51 def enter_handler(self, ui: zf.UI):
52 """
53 Copy the content to clipboard.
54 """
55 pyperclip.copy(self.arg)
56
57
58def create_item(text: str, ui: zf.UI) -> Item:
59 """
60 A helper function to create an item from text.
61 """
62 return Item(
63 title=text,
64 # the ui.terminal is a blessed.Terminal object,
65 # we can use it to add syntax highlight to the text.
66 # you can find more information at https://blessed.readthedocs.io/en/latest/colors.html
67 subtitle=f"hit {ui.terminal.magenta}Enter{ui.terminal.normal} to copy to clipboard",
68 # uid is a unique identifier of the item for internal deduplication
69 # if you don't specify it, it will be generated automatically
70 uid=text,
71 # user can tap "Tab" to autocomplete the query
72 autocomplete=text,
73 # the argument of the item will be used to copy to clipboard
74 arg=text,
75 )
76
77
78zen_of_python = [
79 "Beautiful is better than ugly.",
80 "Explicit is better than implicit.",
81 "Simple is better than complex.",
82 "Complex is better than complicated.",
83 "Flat is better than nested.",
84 "Sparse is better than dense.",
85 "Readability counts.",
86 "Special cases aren't special enough to break the rules.",
87 "Although practicality beats purity.",
88 "Errors should never pass silently.",
89 "Unless explicitly silenced.",
90 "In the face of ambiguity, refuse the temptation to guess.",
91 "There should be one-- and preferably only one --obvious way to do it.",
92 "Although that way may not be obvious at first unless you're Dutch.",
93 "Now is better than never.",
94 "Although never is often better than *right* now.",
95 "If the implementation is hard to explain, it's a bad idea.",
96 "If the implementation is easy to explain, it may be a good idea.",
97 "Namespaces are one honking great idea -- let's do more of those!",
98]
99
100
101def handler(query: str, ui: zf.UI):
102 """
103 The handler is the core of a Zelfred App. It's a user-defined function
104 that takes the entered query and the UI object as inputs and returns
105 a list of items to render.
106 """
107 # if query is not empty
108 if query:
109 # sort by fuzzy match similarity
110 # you can find more information at: https://github.com/seatgeek/fuzzywuzzy
111 results = process.extract(query, zen_of_python, limit=len(zen_of_python))
112 return [create_item(text, ui) for text, score in results]
113 # if query is empty, return the full list in the original order
114 else:
115 return [create_item(text, ui) for text in zen_of_python]
116
117
118if __name__ == "__main__":
119 # reset the debugger and enable it
120 zf.debugger.reset()
121 zf.debugger.enable()
122
123 # create the UI and run it
124 ui = zf.UI(
125 # tell the UI to use the handler function
126 handler=handler,
127 # capture error and display debug information in the UI,
128 # this is the default behavior, if this is set to False, then it will
129 # raise Exception and stop the program.
130 capture_error=True,
131 )
132 # run the UI
133 ui.run()
Google Search with Suggestion#
Difficulty: Medium
The user types a query and receives a dropdown list of Google search suggestions. The user can then tap “Enter” to perform a Google search in their web browser.
Demo
Source Code
1# -*- coding: utf-8 -*-
2
3"""
4Feature:
5
6The user types a query and receives a dropdown list of Google search suggestions.
7The user can then tap "Enter" to perform a Google search in their web browser.
8
9Difficulty: Medium
10
11Dependencies:
12
13.. code-block:: bash
14
15 pip install requests
16
17Demo: https://asciinema.org/a/616014
18"""
19
20import typing as T
21import dataclasses
22
23# we need this to parse google search API response
24import xml.etree.ElementTree as ET
25
26# we need this to send HTTP request
27import requests
28import zelfred.api as zf
29
30
31@dataclasses.dataclass
32class Item(zf.Item):
33 def enter_handler(self, ui: zf.UI):
34 """
35 Open the url in default web browser.
36 """
37 zf.open_url(self.arg)
38
39
40def encode_query(query: str) -> str:
41 """
42 Encode the query to be used in the url.
43 """
44 return query.replace(" ", "+")
45
46
47class GoogleComplete:
48 """
49 Google complete API caller and parser.
50 """
51
52 google_complete_endpoint = (
53 "https://www.google.com/complete/search?output=toolbar&q={query}"
54 )
55
56 def _encode_endpoint(self, query: str) -> str:
57 """
58 :return: full api url.
59 """
60 query = "+".join([s for s in query.split(" ") if s.strip()])
61 return self.google_complete_endpoint.format(query=query)
62
63 def _parse_response(self, html: str) -> T.List[str]:
64 """
65 :return: list of suggestions.
66 """
67 root = ET.fromstring(html)
68 suggestion_list = list()
69 for suggestion in root.iter("suggestion"):
70 suggestion_list.append(suggestion.attrib["data"])
71 return suggestion_list
72
73 def get(self, query: str) -> T.List[str]:
74 """
75 :return: list of suggestions.
76 """
77 url = self._encode_endpoint(query)
78 html = requests.get(url).text
79 suggestion_list = self._parse_response(html)
80 return suggestion_list
81
82
83google_complete = GoogleComplete()
84
85
86def handler(query: str, ui: zf.UI):
87 """
88 The handler is the core of a Zelfred App. It's a user-defined function
89 that takes the entered query and the UI object as inputs and returns
90 a list of items to render.
91 """
92 # if query is empty, return the full list in the original order
93 if bool(query) is False:
94 return [
95 Item(
96 title="type something to search in google",
97 )
98 ]
99 # if query is not empty
100 else:
101 suggestion_list = google_complete.get(query)
102 return [
103 Item(
104 title=s,
105 subtitle=f"hit 'Enter' to Google search {s!r} in web browser",
106 uid=s,
107 autocomplete=s,
108 # store google search url in arg, so we can access it in enter_handler
109 arg=f"https://www.google.com/search?q={encode_query(s)}",
110 )
111 for s in suggestion_list
112 ]
113
114
115if __name__ == "__main__":
116 # reset the debugger and enable it
117 zf.debugger.reset()
118 zf.debugger.enable()
119
120 # create the UI and run it
121 ui = zf.UI(handler=handler, capture_error=True)
122 ui.run()
Search Google Chrome Bookmark#
Difficulty: Medium
User type query and return a dropdown list of matched Google Chrome bookmarks. User can tap “Enter” to open it in default web browser.
Demo
Source Code
1# -*- coding: utf-8 -*-
2
3"""
4Feature:
5
6User type query and return a dropdown list of matched Google Chrome bookmarks.
7User can tap "Enter" to open it in default web browser.
8
9Difficulty: Medium
10
11Dependencies:
12
13.. code-block:: bash
14
15 pip install sayt==0.6.3
16
17Demo: https://asciinema.org/a/617801
18"""
19
20import typing as T
21import json
22import dataclasses
23from pathlib import Path
24
25# we need this to index and search the data
26import sayt.api as sayt
27
28# import zelfred public API
29import zelfred.api as zf
30
31# define important file paths
32dir_home = Path.home()
33dir_zelfred = dir_home.joinpath(".zelfred")
34dir_root = dir_zelfred.joinpath("app-gallery", "search-google-chrome-bookmark")
35dir_root.mkdir(parents=True, exist_ok=True)
36dir_index = dir_root.joinpath(".index")
37dir_cache = dir_root.joinpath(".cache")
38
39
40def find_google_chrom_bookmark_file_on_windows() -> Path:
41 """
42 Locate the Google Chrome bookmark file on Windows.
43 """
44 path = dir_home.joinpath(
45 "AppData",
46 "Local",
47 "Google",
48 "Chrome",
49 "User Data",
50 "Default",
51 "Bookmarks",
52 )
53 if path.exists():
54 return path
55 else:
56 raise FileNotFoundError
57
58
59def find_google_chrome_bookmark_file_on_mac() -> Path:
60 """
61 Locate the Google Chrome bookmark file on MacOS.
62 """
63 path = dir_home.joinpath(
64 "Library",
65 "Application Support",
66 "Google",
67 "Chrome",
68 "Default",
69 "Bookmarks",
70 )
71 if path.exists():
72 return path
73 else:
74 raise FileNotFoundError
75
76
77def find_google_chrome_bookmark_file() -> Path:
78 """
79 Locate the Google Chrome bookmark file.
80
81 Reference: https://www.howtogeek.com/welcome-to-cybersecurity-awareness-week-2023/
82 """
83 for func in [
84 find_google_chrom_bookmark_file_on_windows,
85 find_google_chrome_bookmark_file_on_mac,
86 ]:
87 try:
88 return func()
89 except FileNotFoundError:
90 pass
91 raise FileNotFoundError
92
93
94@dataclasses.dataclass
95class Bookmark:
96 """
97 Google Chrome bookmark dataclass.
98 """
99
100 name: str
101 url: str
102 name_text: str
103 name_ngram: str
104
105
106def parse_bookmark_file(p: Path) -> T.List[Bookmark]:
107 """
108 extract list of bookmark object from the bookmark file.
109 """
110 data = json.loads(p.read_text())
111 bookmark_dct = data.get("roots", {}).get("bookmark_bar", {})
112
113 def extract_bookmark(
114 node: dict,
115 _bookmark_list: T.Optional[T.List[Bookmark]] = None,
116 ):
117 if _bookmark_list is None:
118 _bookmark_list = list()
119 for dct in node.get("children", []):
120 if "url" in dct:
121 name = dct["name"]
122 bookmark = Bookmark(
123 name=name,
124 name_text=name,
125 name_ngram=name,
126 url=dct["url"],
127 )
128 _bookmark_list.append(bookmark)
129 else:
130 extract_bookmark(dct, _bookmark_list)
131 return _bookmark_list
132
133 return extract_bookmark(node=bookmark_dct)
134
135
136# def downloader():
137# """
138# Return the list of bookmark object for search.
139# """
140# p = find_google_chrome_bookmark_file()
141# bookmark_list = parse_bookmark_file(p)
142# return [dataclasses.asdict(bm) for bm in bookmark_list]
143
144
145def downloader():
146 """
147 This function returns dummy data for demo.
148 """
149 data = [
150 ("Google", "https://www.google.com/"),
151 ("Facebook", "https://www.facebook.com/"),
152 ("Amazon", "https://www.amazon.com/"),
153 ("Apple", "https://www.apple.com/"),
154 ("Linkedin", "https://www.linkedin.com/"),
155 ("Microsoft", "https://www.microsoft.com/"),
156 ]
157 return [
158 dataclasses.asdict(
159 Bookmark(
160 name=name,
161 url=url,
162 name_text=name,
163 name_ngram=name,
164 )
165 )
166 for name, url in data
167 ]
168
169
170# create the search as you type dataset, it will automatically refresh every 5 minutes
171ds = sayt.DataSet(
172 dir_index=dir_index,
173 index_name="google_chrome_bookmark",
174 fields=[
175 sayt.TextField("name_text", stored=False),
176 sayt.NgramWordsField("name_ngram", minsize=2, maxsize=8),
177 sayt.StoredField("name"),
178 sayt.StoredField("url"),
179 ],
180 dir_cache=dir_cache,
181 cache_key="google_chrome_bookmark",
182 cache_tag="google_chrome_bookmark",
183 cache_expire=5 * 60,
184 downloader=downloader,
185)
186
187
188@dataclasses.dataclass
189class UrlItem(zf.Item):
190 """
191 Define the custom item class because we need to override the ``enter_handler``.
192 """
193
194 @classmethod
195 def from_doc(cls, doc: dict):
196 return cls(
197 title=doc["name"],
198 subtitle="hit 'Enter' to open: " + doc["url"],
199 uid=doc["url"],
200 autocomplete=doc["name"],
201 arg=doc["url"],
202 )
203
204 def enter_handler(self, ui: zf.UI):
205 """
206 Open the bookmark in web browser.
207 """
208 zf.open_url(self.arg)
209
210
211def handler(query: str, ui: zf.UI):
212 """
213 The handler is the core of a Zelfred App. It's a user-defined function
214 that takes the entered query and the UI object as inputs and returns
215 a list of items to render.
216 """
217 # if query is not empty
218 if query:
219 docs = ds.search(query)
220 # if query is empty, just list first 20 bookmarks
221 else:
222 docs = ds.search("*")
223
224 # if find matches
225 if len(docs):
226 return [UrlItem.from_doc(doc) for doc in docs]
227 # if no match
228 else:
229 return [
230 UrlItem(
231 title="No result found.",
232 subtitle="check your data",
233 uid="no-result",
234 )
235 ]
236
237
238if __name__ == "__main__":
239 # reset the debugger and enable it
240 zf.debugger.reset()
241 zf.debugger.enable()
242
243 # create the UI and run it
244 ui = zf.UI(handler=handler, capture_error=False)
245 ui.run()
Folder and File Search#
Difficulty: Hard
User can search folder in a root directory, and then tap “Enter” to enter a sub query session to search file in the selected folder. At the end, user can tab “Enter” to open the file using the default application. Also, user can tap “F1” to exit the sub query session and go back to the folder search session.
Demo
Source Code
1# -*- coding: utf-8 -*-
2
3"""
4Feature:
5
6User can search folder in a root directory, and then tap "Enter" to enter
7a sub query session to search file in the selected folder. At the end, user
8can tab "Enter" to open the file using the default application. Also, user can
9tap "F1" to exit the sub query session and go back to the folder search session.
10
11Difficulty: Hard
12
13Dependencies:
14
15.. code-block:: bash
16
17 pip install fuzzywuzzy>=0.18.0,<1.0.0
18 pip install python-Levenshtein>=0.21.0,<1.0.0
19
20Demo: https://asciinema.org/a/616119
21"""
22
23import typing as T
24import dataclasses
25from pathlib import Path
26
27from fuzzywuzzy import process
28import zelfred.api as zf
29
30
31@dataclasses.dataclass
32class OpenFileActionItem(zf.Item):
33 def enter_handler(self, ui: zf.UI):
34 """
35 Open file in default application.
36 """
37 zf.open_file(Path(self.arg))
38
39
40@dataclasses.dataclass
41class CopyFilePathActionItem(zf.Item):
42 def enter_handler(self, ui: zf.UI):
43 """
44 Copy file path.
45 """
46 print(
47 f"copied {self.arg!r} path to clipboard (not really copied, just for demo purpose)"
48 )
49
50
51@dataclasses.dataclass
52class CopyFileContentActionItem(zf.Item):
53 def enter_handler(self, ui: zf.UI):
54 """
55 Copy file content.
56 """
57 content = Path(self.arg).read_text()
58 print(
59 f"copied {self.arg!r} file content {content!r} to clipboard (not really copied, just for demo purpose)"
60 )
61
62
63@dataclasses.dataclass
64class FileItem(zf.Item):
65 """
66 Represent a file in the dropdown menu.
67 """
68
69 @classmethod
70 def from_names(cls, name_list: T.List[str], folder: str) -> T.List["FileItem"]:
71 """
72 Convert a file name list to a list of items. The file name
73 will become the title, uid, autocomplete and the arg.
74 """
75 return [
76 cls(
77 uid=name,
78 title=name,
79 subtitle=f"hit 'Enter' to open this file",
80 arg=str(dir_home.joinpath(folder, name)),
81 autocomplete=name,
82 )
83 for name in name_list
84 ]
85
86 def enter_handler(self, ui: zf.UI):
87 """
88 Open file in default application.
89 """
90 # define the new handler function for the sub query session
91 path = Path(self.arg)
92
93 def sub_handler(query: str, ui: zf.UI):
94 """
95 A partial function that using the given folder.
96 """
97 return handler_file_action(path, query, ui)
98
99 # run the sub session using the new handler
100 # user can tap 'F1' to exit the sub query session,
101 # and go back to the folder selection session.
102 ui.run_sub_session(handler=sub_handler, initial_query="")
103
104
105@dataclasses.dataclass
106class FolderItem(zf.Item):
107 """
108 Represent a folder in the dropdown menu.
109 """
110
111 @classmethod
112 def from_names(cls, name_list: T.List[str]) -> T.List["FolderItem"]:
113 """
114 Convert a folder name list to a list of items. The folder name
115 will become the title, uid, autocomplete and the arg.
116 """
117 return [
118 cls(
119 title=name,
120 subtitle=f"hit 'Enter' to search file in this folder",
121 uid=name,
122 autocomplete=name, # allow user to tap 'Tab' to auto-complete
123 arg=name, # the argument of the folder item will be used to list files
124 )
125 for name in name_list
126 ]
127
128 def enter_handler(self, ui: zf.UI):
129 """
130 Enter a sub query session.
131
132 .. note::
133
134 THIS IS A VERY GOOD EXAMPLE OF HOW TO ENTER A SUB QUERY SESSION.
135
136 your main UI loop has a handler, sub query session too. So you need
137 to define a new handler function for the sub query session, and then
138 use the ``ui.run_sub_session()`` method to enter the sub session.
139 You can also use ``initial_query`` argument to set the start-up
140 input query to display in the line editor.
141 """
142 # define the new handler function for the sub query session
143 folder = self.arg
144
145 def sub_handler(query: str, ui: zf.UI):
146 """
147 A partial function that using the given folder.
148 """
149 return handler_file(folder, query, ui)
150
151 # run the sub session using the new handler
152 # user can tap 'F1' to exit the sub query session,
153 # and go back to the folder selection session.
154 ui.run_sub_session(handler=sub_handler, initial_query="")
155
156
157dir_home = Path(__file__).absolute().parent.joinpath("home")
158
159
160def handler_file_action(path: Path, query: str, ui: zf.UI):
161 """
162 This is the handler for the nested sub query (sub query in sub query) session.
163
164 Given a path, you have to create a partial function that using the given
165 path. The partial function will become the final handler of the sub query.
166
167 This handler returns couple of action you can do with the file.
168 """
169 return [
170 OpenFileActionItem(
171 title="Open file",
172 subtitle=f"hit 'Enter' to open file",
173 uid="open-file",
174 autocomplete=path.name, # allow user to tap 'Tab' to auto-complete
175 arg=str(
176 path
177 ), # the argument of OpenFileActionItem will be used to open file
178 ),
179 CopyFilePathActionItem(
180 title="Copy file path",
181 subtitle=f"hit 'Enter' to copy file path",
182 uid="copy-file-path",
183 autocomplete=path.name, # allow user to tap 'Tab' to auto-complete
184 arg=str(
185 path
186 ), # the argument of OpenFileActionItem will be used to open file
187 ),
188 CopyFileContentActionItem(
189 title="Copy file content",
190 subtitle=f"hit 'Enter' to copy file content",
191 uid="copy-file-content",
192 autocomplete=path.name, # allow user to tap 'Tab' to auto-complete
193 arg=str(
194 path
195 ), # the argument of OpenFileActionItem will be used to open file
196 ),
197 ]
198
199
200def handler_file(folder: str, query: str, ui: zf.UI):
201 """
202 This is the handler for the sub query session.
203
204 Given a folder, you have to create a partial function that using the given
205 folder. The partial function will become the final handler of the sub query.
206 """
207 file_list = [p.name for p in dir_home.joinpath(folder).iterdir()]
208 # if query is not empty
209 if query:
210 # sort by fuzzy match similarity
211 results = process.extract(query, file_list, limit=len(file_list))
212 return FileItem.from_names([file for file, score in results], folder)
213 # if query is empty, return the full list in the original order
214 else:
215 return FileItem.from_names(file_list, folder)
216
217
218def handler_folder(query: str, ui: zf.UI):
219 """
220 This is the handler for folder selection.
221 """
222 folder_list = [p.name for p in dir_home.iterdir()]
223 # if query is not empty
224 if query:
225 # sort by fuzzy match similarity
226 results = process.extract(query, folder_list, limit=len(folder_list))
227 return FolderItem.from_names([folder for folder, score in results])
228 # if query is empty, return the full list in the original order
229 else:
230 return FolderItem.from_names(folder_list)
231
232
233if __name__ == "__main__":
234 # reset the debugger and enable it
235 zf.debugger.reset()
236 zf.debugger.enable()
237
238 # create the UI and run it
239 ui = zf.UI(handler=handler_folder, capture_error=True)
240 ui.run()
Password Book App#
Difficulty: Hard
User the user input to search the username, allow user to tap “Ctrl A” to copy the password to clipboard. Afterward, the UI doesn’t exit and wait for the next user input.
Demo
Source Code
Refresh Cache V1#
Difficulty: Medium
No matter what user entered, always return a random value between 1 and 100. And this value is based on cache that won’t change while user is typing. However, we want to provide a way to refresh the value. User can type “!~” and then hit “ENTER” to refresh the value. When user hit ENTER, it automatically removes the “!~” part and recover the original query.
Demo
Source Code
1# -*- coding: utf-8 -*-
2
3"""
4Feature:
5
6No matter what user entered, always return a random value between 1 and 100.
7And this value is based on cache that won't change while user is typing.
8However, we want to provide a way to refresh the value. User can type "!~"
9and then hit ENTER to refresh the value. When user hit ENTER, it automatically
10removes the "!~" part and recover the original query.
11
12Difficulty: Hard
13
14Dependencies: NA
15
16Demo: https://asciinema.org/a/631197
17"""
18
19import random
20import dataclasses
21
22import zelfred.api as zf
23
24
25@dataclasses.dataclass
26class RefreshItem(zf.Item):
27 """
28 Represent an item that can refresh cache in the dropdown menu.
29 """
30
31 def enter_handler(self, ui: zf.UI):
32 """
33 Copy the content to clipboard.
34 """
35 cache.value = None
36
37 def post_enter_handler(self, ui: zf.UI):
38 """
39 After the user input action, we would like to repaint the dropdown menu
40 only to show the "Copied" item, and then wait for the next user input.
41 """
42 ui.line_editor.move_to_end()
43 ui.line_editor.press_backspace(self.variables["n_backspace"])
44
45
46class Cache:
47 def __init__(self):
48 self.value = None
49
50
51cache = Cache()
52
53
54def handler(query: str, ui: zf.UI):
55 """
56 The handler is the core of a Zelfred App. It's a user-defined function
57 that takes the entered query and the UI object as inputs and returns
58 a list of items to render.
59 """
60 # if query is for refresh value
61 if "!~" in query:
62 before_query, after_query = query.split("!~", 1)
63 return [
64 RefreshItem(
65 title="Refresh value",
66 subtitle=f"Hit {ui.render.ENTER} to refresh the value",
67 variables={"n_backspace": len(after_query) + 2},
68 ),
69 ]
70 # otherwise, always return a random value, and cache it
71 else:
72 if cache.value is None:
73 cache.value = random.randint(1, 100)
74 return [
75 zf.Item(
76 title=f"Value {cache.value}",
77 subtitle=f"Type !~ to refresh the value",
78 )
79 ]
80
81
82if __name__ == "__main__":
83 # reset the debugger and enable it
84 zf.debugger.reset()
85 zf.debugger.enable()
86
87 # create the UI and run it
88 ui = zf.UI(handler=handler, capture_error=True)
89 ui.run()
Refresh Cache V2#
Difficulty: Medium
No matter what user entered, always return a random value between 1 and 100. And this value is based on cache that won’t change while user is typing. However, we want to provide a way to refresh the value. User can type “!~”, then the value will be immediately refreshed, and the “!~” will be removed automatically.
Demo
Source Code
1# -*- coding: utf-8 -*-
2
3"""
4Feature:
5
6No matter what user entered, always return a random value between 1 and 100.
7And this value is based on cache that won't change while user is typing.
8However, we want to provide a way to refresh the value. User can type "!~",
9then the value will be immediately refreshed, and the "!~" will be removed
10automatically.
11
12Difficulty: Medium
13
14Dependencies: NA
15
16Demo: https://asciinema.org/a/631325
17"""
18
19import random
20
21import zelfred.api as zf
22
23
24class Cache:
25 def __init__(self):
26 self.value = None
27
28
29cache = Cache()
30
31
32def handler(query: str, ui: zf.UI):
33 """
34 The handler is the core of a Zelfred App. It's a user-defined function
35 that takes the entered query and the UI object as inputs and returns
36 a list of items to render.
37 """
38 # if query is for refresh value
39 if "!~" in query:
40 # refresh the value
41 cache.value = random.randint(1, 100)
42 # remove the "!~" from the query in the UI
43 ui.line_editor.press_backspace(2)
44 return [
45 zf.Item(
46 title=f"Value {cache.value}",
47 subtitle=f"Type !~ to refresh the value",
48 )
49 ]
50 # otherwise, always return a random value, and cache it
51 else:
52 if cache.value is None:
53 cache.value = random.randint(1, 100)
54 return [
55 zf.Item(
56 title=f"Value {cache.value}",
57 subtitle=f"Type !~ to refresh the value",
58 )
59 ]
60
61
62if __name__ == "__main__":
63 # reset the debugger and enable it
64 zf.debugger.reset()
65 zf.debugger.enable()
66
67 # create the UI and run it
68 ui = zf.UI(handler=handler, capture_error=True)
69 ui.run()
Refresh Cache V3#
Difficulty: Medium
No matter what user entered, always return a random value between 1 and 100. And this value is based on cache that won’t change while user is typing. However, we want to provide a way to refresh the value. User can type “!~”, then the value will be refreshed after 1 seconds, and the “!~” will be removed automatically. During the waiting, it will show a helper text to tell user to wait.
Demo
Source Code
1# -*- coding: utf-8 -*-
2
3"""
4Feature:
5
6No matter what user entered, always return a random value between 1 and 100.
7And this value is based on cache that won't change while user is typing.
8However, we want to provide a way to refresh the value. User can type "!~",
9then the value will be refreshed after 1 seconds, and the "!~" will be removed
10automatically. During the waiting, it will show a helper text to tell user to wait.
11
12Difficulty: Medium
13
14Dependencies: NA
15
16Demo: https://asciinema.org/a/631335
17"""
18
19import time
20import random
21
22import zelfred.api as zf
23
24
25class Cache:
26 def __init__(self):
27 self.value = None
28
29
30cache = Cache()
31
32
33def handler(query: str, ui: zf.UI):
34 """
35 The handler is the core of a Zelfred App. It's a user-defined function
36 that takes the entered query and the UI object as inputs and returns
37 a list of items to render.
38 """
39 # if query is for refresh value
40 if "!~" in query:
41 # show a helper text
42 items = [
43 zf.Item(
44 title=f"Let's wait 1 seconds for refreshing",
45 subtitle="please don't type anything",
46 )
47 ]
48 # explicitly set the items
49 ui.run_handler(items=items)
50 ui.repaint()
51 # run the refresh logic, it may take a while
52 time.sleep(1)
53 # refresh the value
54 cache.value = random.randint(1, 100)
55 # remove the "!~" from the query in the UI
56 ui.line_editor.press_backspace(2)
57 return [
58 zf.Item(
59 title=f"Value {cache.value}",
60 subtitle=f"Type !~ to refresh the value",
61 )
62 ]
63 # otherwise, always return a random value, and cache it
64 else:
65 if cache.value is None:
66 cache.value = random.randint(1, 100)
67 return [
68 zf.Item(
69 title=f"Value {cache.value}",
70 subtitle=f"Type !~ to refresh the value",
71 )
72 ]
73
74
75if __name__ == "__main__":
76 # reset the debugger and enable it
77 zf.debugger.reset()
78 zf.debugger.enable()
79
80 # create the UI and run it
81 ui = zf.UI(handler=handler, capture_error=True)
82 ui.run()
JSON Formatter#
Difficulty: Easy
Copy JSON text to clipboard, then hit ‘Enter’ to dump the formatted JSON to
${HOME}/tmp/formatted.json
and automatically open it.
Demo
Source Code
1# -*- coding: utf-8 -*-
2
3"""
4Feature:
5
6Copy JSON text to clipboard, then hit 'Enter' to dump the formatted JSON to
7``${HOME}/tmp/formatted.json`` and automatically open it.
8
9Difficulty: Easy
10
11Dependencies: NA
12
13Demo: https://asciinema.org/a/123456
14"""
15
16import json
17import dataclasses
18from pathlib import Path
19
20import pyperclip
21import zelfred.api as zf
22
23p = Path.home().joinpath("tmp", "formatted.json")
24p.parent.mkdir(exist_ok=True)
25
26
27@dataclasses.dataclass
28class JsonFormatterItem(zf.Item):
29 """
30 Represent a json formatter item in the dropdown menu.
31 """
32
33 def enter_handler(self, ui: zf.UI):
34 """
35 Read json from clipboard, format it, write to ~/tmp/formatted.json, then open it.
36 """
37 s = pyperclip.paste()
38 p.write_text(json.dumps(json.loads(s), indent=4))
39 zf.open_file(p)
40
41
42def handler(query: str, ui: zf.UI):
43 """
44 The handler is the core of a Zelfred App. It's a user-defined function
45 that takes the entered query and the UI object as inputs and returns
46 a list of items to render.
47 """
48 return [
49 JsonFormatterItem(
50 title=f"Hit 'Enter' to format your JSON",
51 subtitle="hint: copy your JSON to clipboard before you hit 'Enter'",
52 )
53 ]
54
55
56if __name__ == "__main__":
57 # reset the debugger and enable it
58 zf.debugger.reset()
59 zf.debugger.enable()
60
61 # create the UI and run it
62 ui = zf.UI(handler=handler, capture_error=True)
63 ui.run()