By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 main.py

View raw Download
text/plain • 12.77 kiB
Python script, ASCII text executable
        
            
1
"""
2
Izvor - modular, GTK+ desktop search and launcher
3
Copyright (C) 2024 <root@roundabout-host.com>
4
5
This program is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public License as published by
7
the Free Software Foundation, either version 3 of the License, or
8
(at your option) any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
GNU General Public License for more details.
14
15
You should have received a copy of the GNU General Public License
16
along with this program. If not, see <https://www.gnu.org/licenses/>.
17
"""
18
19
import os
20
import gi
21
import asyncio
22
import importlib
23
import gbulb
24
from pathlib import Path
25
from typing import AsyncIterable, AsyncIterator, Collection, TypeVar, Iterable, Callable, Any
26
27
from twisted.conch.insults.text import attributes
28
29
gi.require_version("Gtk", "3.0")
30
from gi.repository import Gtk, Gdk, Pango, GLib
31
32
33
_T = TypeVar("_T")
34
35
ICON_SIZE = 32
36
37
38
async def merge_generators_with_params(generator_funcs: list[Callable[..., AsyncIterable]],
39
*args, **kwargs):
40
iterators = [gen_func(*args, **kwargs).__aiter__() for gen_func in generator_funcs]
41
tasks = {asyncio.create_task(it.__anext__()): it for it in iterators}
42
43
while tasks:
44
done, _ = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED)
45
46
for task in done:
47
iterator = tasks.pop(task)
48
try:
49
result = task.result()
50
yield result
51
tasks[asyncio.create_task(iterator.__anext__())] = iterator
52
except StopAsyncIteration:
53
pass
54
55
56
class ResultInfoWidget(Gtk.ListBoxRow):
57
def __init__(self, name: str, description: str, image: tuple[str, Any], execute: Callable[[], None]):
58
super().__init__()
59
self.name = name
60
self.description = description
61
self.image = image
62
self.execute = execute
63
64
self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
65
self.add(self.box)
66
67
if image[0] == "logo":
68
self.image = Gtk.Image.new_from_icon_name(self.image[1], Gtk.IconSize.LARGE_TOOLBAR)
69
self.image.set_pixel_size(ICON_SIZE)
70
self.box.pack_start(self.image, False, False, 0)
71
72
self.label_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
73
self.box.pack_start(self.label_box, True, True, 0)
74
75
self.name_label = Gtk.Label(label=self.name)
76
self.name_label.set_line_wrap(True)
77
self.name_label.set_justify(Gtk.Justification.LEFT)
78
self.name_label.set_halign(Gtk.Align.START)
79
attributes = Pango.AttrList()
80
attributes.insert(Pango.AttrSize.new(16 * Pango.SCALE))
81
self.name_label.set_attributes(attributes)
82
self.label_box.pack_start(self.name_label, True, True, 0)
83
84
self.description_label = Gtk.Label(label=self.description)
85
self.description_label.set_line_wrap(True)
86
self.description_label.set_justify(Gtk.Justification.LEFT)
87
self.description_label.set_halign(Gtk.Align.START)
88
self.label_box.pack_start(self.description_label, True, True, 0)
89
90
91
class Izvor(Gtk.Application):
92
USER_CONFIGS = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) / "izvor"
93
USER_DATA = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "izvor"
94
USER_PROVIDERS = USER_DATA / "providers"
95
USER_PROVIDER_CONFIGS = USER_CONFIGS / "providers"
96
SYSTEM_CONFIGS = Path("/etc/izvor")
97
SYSTEM_DATA = Path("/usr/share/izvor")
98
SYSTEM_PROVIDERS = SYSTEM_DATA / "providers"
99
SYSTEM_PROVIDER_CONFIGS = SYSTEM_CONFIGS / "providers"
100
ENABLED_PROVIDERS_FILE = USER_CONFIGS / "enabled_providers"
101
102
def __init__(self):
103
super().__init__(application_id="com.roundabout-host.roundabout.izvor")
104
gbulb.install(gtk=True)
105
self.loop = asyncio.get_event_loop()
106
self.builder = Gtk.Builder()
107
self.builder.add_from_file("izvor.ui")
108
self.window = self.builder.get_object("root")
109
self.window.connect("destroy", self.kill)
110
self.window.connect("key-press-event", self.check_escape)
111
self.window.set_title("Izvor")
112
113
if not os.getenv("XDG_DATA_HOME"):
114
print("XDG_DATA_HOME is not set. Using default path.")
115
if not os.getenv("XDG_CONFIG_HOME"):
116
print("XDG_CONFIG_HOME is not set. Using default path.")
117
if not self.USER_PROVIDER_CONFIGS.exists():
118
print("Creating user config directory.")
119
self.USER_PROVIDER_CONFIGS.mkdir(parents=True)
120
if not self.USER_PROVIDERS.exists():
121
print("Creating user data directory.")
122
self.USER_PROVIDERS.mkdir(parents=True)
123
124
self.available_providers = {}
125
126
if self.SYSTEM_PROVIDERS.exists():
127
for provider in self.SYSTEM_PROVIDERS.iterdir():
128
if not (provider / "__init__.py").exists():
129
continue
130
self.available_providers[provider.stem] = provider
131
if self.USER_PROVIDERS.exists():
132
for provider in self.USER_PROVIDERS.iterdir():
133
if not (provider / "__init__.py").exists():
134
continue
135
self.available_providers[provider.stem] = provider
136
137
print(f"Available providers: {list(self.available_providers.keys())}")
138
139
if not self.ENABLED_PROVIDERS_FILE.exists():
140
self.ENABLED_PROVIDERS_FILE.touch()
141
142
with open(self.ENABLED_PROVIDERS_FILE, "w") as f:
143
for provider in self.available_providers:
144
f.write(provider + "\n")
145
146
self.permanently_enabled_providers = {}
147
148
with open(self.ENABLED_PROVIDERS_FILE, "r") as f:
149
for line in f:
150
provider = line.strip()
151
if provider in self.available_providers:
152
self.permanently_enabled_providers[provider] = self.available_providers[provider]
153
154
self.enabled_providers = self.permanently_enabled_providers.copy()
155
156
providers_menu = self.builder.get_object("providers-menu")
157
158
self.providers = []
159
self.provider_checkboxes = {}
160
161
for provider in self.permanently_enabled_providers.values():
162
spec = importlib.util.spec_from_file_location(provider.stem, provider / "__init__.py")
163
module = importlib.util.module_from_spec(spec)
164
spec.loader.exec_module(module)
165
loaded_provider = module.Provider({})
166
self.providers.append(loaded_provider)
167
print(f"Loaded provider: {loaded_provider.name}")
168
print(loaded_provider.description)
169
self.provider_checkboxes[provider.stem] = Gtk.CheckMenuItem.new_with_label(loaded_provider.name)
170
providers_menu.append(self.provider_checkboxes[provider.stem])
171
self.provider_checkboxes[provider.stem].set_active(True)
172
self.provider_checkboxes[provider.stem].show()
173
self.provider_checkboxes[provider.stem].connect("toggled", self.update_enabled_providers)
174
175
def call_update_results(widget):
176
asyncio.create_task(self.update_results(widget))
177
178
def execute_result(widget, row):
179
row.execute()
180
self.kill()
181
182
self.builder.connect_signals(
183
{
184
"update-search": call_update_results,
185
"result-activated": execute_result,
186
"providers-menu-all-toggled": self.update_enabled_providers_all,
187
"menu-about": self.about,
188
"menu-providers": self.provider_menu,
189
}
190
)
191
192
GLib.idle_add(self.update_enabled_providers, None)
193
194
def update_enabled_providers(self, widget=None):
195
for provider, checkbox in self.provider_checkboxes.items():
196
if checkbox.get_active():
197
self.enabled_providers[provider] = self.permanently_enabled_providers[provider]
198
else:
199
self.enabled_providers.pop(provider, None)
200
201
asyncio.create_task(self.update_results(None))
202
203
def update_enabled_providers_all(self, widget):
204
for checkbox in self.provider_checkboxes.values():
205
checkbox.set_active(widget.get_active())
206
self.update_enabled_providers()
207
208
def kill(self, widget=None):
209
self.loop.stop()
210
self.quit()
211
212
def check_escape(self, widget, event):
213
if event.keyval == 65307:
214
self.kill()
215
216
def about(self, widget):
217
about_builder = Gtk.Builder()
218
about_builder.add_from_file("about.ui")
219
about_window = about_builder.get_object("about-dialog")
220
about_window.connect("destroy", lambda _: about_window.destroy())
221
about_window.connect("close", lambda _: about_window.destroy())
222
about_window.connect("response", lambda _, __: about_window.destroy())
223
about_window.show_all()
224
225
def provider_menu(self, widget):
226
providers_builder = Gtk.Builder()
227
providers_builder.add_from_file("providers.ui")
228
providers_window = providers_builder.get_object("providers-window")
229
providers_window.connect("destroy", lambda _: providers_window.destroy())
230
231
providers_table = providers_builder.get_object("providers-table") # actually Gtk.Grid
232
rows = 0
233
for provider in self.available_providers.values():
234
spec = importlib.util.spec_from_file_location(provider.stem, provider / "__init__.py")
235
module = importlib.util.module_from_spec(spec)
236
spec.loader.exec_module(module)
237
loaded_provider = module.Provider({})
238
239
provider_label = Gtk.Label(label=loaded_provider.name)
240
provider_label.set_halign(Gtk.Align.START)
241
provider_label.set_valign(Gtk.Align.CENTER)
242
font_desc = Pango.FontDescription()
243
font_desc.set_weight(Pango.Weight.BOLD)
244
provider_label.override_font(font_desc)
245
provider_label.set_line_wrap(True)
246
provider_label.set_justify(Gtk.Justification.LEFT)
247
248
provider_description = Gtk.Label(label=loaded_provider.description)
249
provider_description.set_halign(Gtk.Align.START)
250
provider_description.set_valign(Gtk.Align.CENTER)
251
provider_description.set_justify(Gtk.Justification.LEFT)
252
provider_description.set_line_wrap(True)
253
254
icon = Gtk.Image.new_from_icon_name(loaded_provider.icon, Gtk.IconSize.LARGE_TOOLBAR)
255
icon.set_pixel_size(ICON_SIZE)
256
257
switch = Gtk.Switch()
258
switch.set_valign(Gtk.Align.CENTER)
259
switch.set_active(provider.stem in self.permanently_enabled_providers)
260
switch.connect("notify::active", self.update_permanently_enabled_providers, provider.stem)
261
262
providers_table.attach(icon, 0, rows, 1, 1)
263
providers_table.attach(provider_label, 1, rows, 1, 1)
264
providers_table.attach(provider_description, 2, rows, 1, 1)
265
providers_table.attach(switch, 3, rows, 1, 1)
266
267
provider_label.show()
268
provider_description.show()
269
270
rows += 1
271
272
providers_window.show_all()
273
274
275
def update_permanently_enabled_providers(self, widget, param, provider):
276
if widget.get_active():
277
self.permanently_enabled_providers[provider] = self.available_providers[provider]
278
else:
279
self.permanently_enabled_providers.pop(provider, None)
280
281
with open(self.ENABLED_PROVIDERS_FILE, "w") as f:
282
f.write("\n".join(self.permanently_enabled_providers.keys()))
283
284
285
async def update_results(self, widget):
286
print("Updating results...")
287
generators = []
288
query = self.builder.get_object("search-query-buffer").get_text()
289
# print(f"Query: {query}")
290
for provider in self.enabled_providers.values():
291
spec = importlib.util.spec_from_file_location(provider.stem, provider / "__init__.py")
292
module = importlib.util.module_from_spec(spec)
293
spec.loader.exec_module(module)
294
loaded_provider = module.Provider({})
295
# print(f"Searching with provider: {loaded_provider.name}")
296
297
generators.append(loaded_provider.search)
298
299
# Clear the results list
300
results_list = self.builder.get_object("results-list")
301
for row in results_list.get_children():
302
results_list.remove(row)
303
304
async for result in merge_generators_with_params(generators, query):
305
# print(result)
306
result_box = ResultInfoWidget(result["name"], result["description"], result["image"], result["execute"])
307
results_list.add(result_box)
308
result_box.show_all()
309
310
results_list.show_all()
311
312
313
if __name__ == "__main__":
314
izvor = Izvor()
315
izvor.window.show_all()
316
try:
317
izvor.loop.run_forever()
318
finally:
319
izvor.loop.close()
320