main.py
Python script, ASCII text executable
1""" 2Izvor - modular, GTK+ desktop search and launcher 3Copyright (C) 2024 <root@roundabout-host.com> 4 5This program is free software: you can redistribute it and/or modify 6it under the terms of the GNU General Public License as published by 7the Free Software Foundation, either version 3 of the License, or 8(at your option) any later version. 9 10This program is distributed in the hope that it will be useful, 11but WITHOUT ANY WARRANTY; without even the implied warranty of 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13GNU General Public License for more details. 14 15You should have received a copy of the GNU General Public License 16along with this program. If not, see <https://www.gnu.org/licenses/>. 17""" 18 19import os 20import gi 21import asyncio 22import importlib 23import gbulb 24from pathlib import Path 25from typing import AsyncIterable, AsyncIterator, Collection, TypeVar, Iterable, Callable, Any 26 27gi.require_version("Gtk", "3.0") 28from gi.repository import Gtk, Gdk, Pango 29 30 31_T = TypeVar("_T") 32 33ICON_SIZE = 32 34 35 36async def merge_generators_with_params(generator_funcs: list[Callable[..., AsyncIterable]], 37*args, **kwargs): 38iterators = [gen_func(*args, **kwargs).__aiter__() for gen_func in generator_funcs] 39tasks = {asyncio.create_task(it.__anext__()): it for it in iterators} 40 41while tasks: 42done, _ = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED) 43 44for task in done: 45iterator = tasks.pop(task) 46try: 47result = task.result() 48yield result 49tasks[asyncio.create_task(iterator.__anext__())] = iterator 50except StopAsyncIteration: 51pass 52 53 54class ResultInfoWidget(Gtk.ListBoxRow): 55def __init__(self, name: str, description: str, image: tuple[str, Any], execute: Callable[[], None]): 56super().__init__() 57self.name = name 58self.description = description 59self.image = image 60self.execute = execute 61 62self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) 63self.add(self.box) 64 65if image[0] == "logo": 66self.image = Gtk.Image.new_from_icon_name(self.image[1], Gtk.IconSize.LARGE_TOOLBAR) 67self.image.set_pixel_size(ICON_SIZE) 68self.box.pack_start(self.image, False, False, 0) 69 70self.label_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 71self.box.pack_start(self.label_box, True, True, 0) 72 73self.name_label = Gtk.Label(label=self.name) 74self.name_label.set_line_wrap(True) 75self.name_label.set_justify(Gtk.Justification.LEFT) 76self.name_label.set_halign(Gtk.Align.START) 77attributes = Pango.AttrList() 78attributes.insert(Pango.AttrSize.new(16 * Pango.SCALE)) 79self.name_label.set_attributes(attributes) 80self.label_box.pack_start(self.name_label, True, True, 0) 81 82self.description_label = Gtk.Label(label=self.description) 83self.description_label.set_line_wrap(True) 84self.description_label.set_justify(Gtk.Justification.LEFT) 85self.description_label.set_halign(Gtk.Align.START) 86self.label_box.pack_start(self.description_label, True, True, 0) 87 88 89class Izvor: 90USER_CONFIGS = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) / "izvor" 91USER_DATA = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "izvor" 92USER_PROVIDERS = USER_DATA / "providers" 93USER_PROVIDER_CONFIGS = USER_CONFIGS / "providers" 94SYSTEM_CONFIGS = Path("/etc/izvor") 95SYSTEM_DATA = Path("/usr/share/izvor") 96SYSTEM_PROVIDERS = SYSTEM_DATA / "providers" 97SYSTEM_PROVIDER_CONFIGS = SYSTEM_CONFIGS / "providers" 98 99def __init__(self): 100self.builder = Gtk.Builder() 101self.builder.add_from_file("izvor.ui") 102self.window = self.builder.get_object("root") 103self.window.connect("destroy", Gtk.main_quit) 104self.window.connect("key-press-event", self.check_escape) 105 106if not os.getenv("XDG_DATA_HOME"): 107print("XDG_DATA_HOME is not set. Using default path.") 108if not os.getenv("XDG_CONFIG_HOME"): 109print("XDG_CONFIG_HOME is not set. Using default path.") 110if not self.USER_PROVIDER_CONFIGS.exists(): 111print("Creating user config directory.") 112self.USER_PROVIDER_CONFIGS.mkdir(parents=True) 113if not self.USER_PROVIDERS.exists(): 114print("Creating user data directory.") 115self.USER_PROVIDERS.mkdir(parents=True) 116 117self.available_providers = {} 118 119if self.SYSTEM_PROVIDERS.exists(): 120for provider in self.SYSTEM_PROVIDERS.iterdir(): 121if not (provider / "__init__.py").exists(): 122continue 123self.available_providers[provider.stem] = provider 124if self.USER_PROVIDERS.exists(): 125for provider in self.USER_PROVIDERS.iterdir(): 126if not (provider / "__init__.py").exists(): 127continue 128self.available_providers[provider.stem] = provider 129 130print(f"Available providers: {list(self.available_providers.keys())}") 131 132self.enabled_providers = self.available_providers.copy() 133 134providers_menu = self.builder.get_object("providers-menu") 135 136self.providers = [] 137self.provider_checkboxes = {} 138 139for provider in self.enabled_providers.values(): 140spec = importlib.util.spec_from_file_location(provider.stem, provider / "__init__.py") 141module = importlib.util.module_from_spec(spec) 142spec.loader.exec_module(module) 143loaded_provider = module.Provider({}) 144self.providers.append(loaded_provider) 145print(f"Loaded provider: {loaded_provider.name}") 146print(loaded_provider.description) 147self.provider_checkboxes[loaded_provider.name] = Gtk.CheckMenuItem.new_with_label(loaded_provider.name) 148providers_menu.append(self.provider_checkboxes[loaded_provider.name]) 149self.provider_checkboxes[loaded_provider.name].set_active(True) 150self.provider_checkboxes[loaded_provider.name].show() 151 152def call_update_results(widget): 153asyncio.create_task(self.update_results(widget)) 154 155def execute_result(widget, row): 156row.execute() 157 158self.builder.connect_signals( 159{ 160"update-search": call_update_results, 161"result-activated": execute_result, 162} 163) 164 165def check_escape(self, widget, event): 166if event.keyval == 65307: 167self.window.destroy() 168 169async def update_results(self, widget): 170print("Updating results...") 171generators = [] 172query = self.builder.get_object("search-query-buffer").get_text() 173# print(f"Query: {query}") 174for provider in self.enabled_providers.values(): 175spec = importlib.util.spec_from_file_location(provider.stem, provider / "__init__.py") 176module = importlib.util.module_from_spec(spec) 177spec.loader.exec_module(module) 178loaded_provider = module.Provider({}) 179# print(f"Searching with provider: {loaded_provider.name}") 180 181generators.append(loaded_provider.search) 182 183# Clear the results list 184results_list = self.builder.get_object("results-list") 185for row in results_list.get_children(): 186results_list.remove(row) 187 188async for result in merge_generators_with_params(generators, query): 189# print(result) 190result_box = ResultInfoWidget(result["name"], result["description"], result["image"], result["execute"]) 191results_list.add(result_box) 192result_box.show_all() 193 194results_list.show_all() 195 196 197if __name__ == "__main__": 198gbulb.install(gtk=True) 199loop = asyncio.get_event_loop() 200izvor = Izvor() 201izvor.window.show_all() 202loop.run_forever() 203