main.py
Python script, ASCII text executable
1import os 2import sys 3import importlib 4from itertools import accumulate, chain 5from pathlib import Path 6import ruamel.yaml as yaml 7 8os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0" 9 10from ctypes import CDLL 11CDLL('libgtk4-layer-shell.so') 12 13import gi 14gi.require_version("Gtk", "4.0") 15gi.require_version("Gtk4LayerShell", "1.0") 16 17from gi.repository import Gtk, GLib, Gtk4LayerShell, Gdk, Gio 18 19sys.path.insert(0, str((Path(__file__).parent / "shared").resolve())) 20 21import panorama_panel 22 23 24def get_applet_directories(): 25data_home = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share")) 26data_dirs = [Path(d) for d in os.getenv("XDG_DATA_DIRS", "/usr/local/share:/usr/share").split(":")] 27 28all_paths = [data_home / "panorama-panel" / "applets"] + [d / "panorama-panel" / "applets" for d in data_dirs] 29return [d for d in all_paths if d.is_dir()] 30 31 32def get_config_file(): 33config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) 34 35return config_home / "panorama-panel" / "config.yaml" 36 37 38panels = [] 39 40 41class ManagerWindow(Gtk.Window): 42def __init__(self): 43super().__init__() 44box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 45self.set_child(box) 46switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 47self.edit_mode_switch = Gtk.Switch() 48switch_box.append(self.edit_mode_switch) 49edit_mode_label = Gtk.Label() 50edit_mode_label.set_text("Panel editing") 51edit_mode_label.set_mnemonic_widget(self.edit_mode_switch) 52switch_box.append(edit_mode_label) 53box.append(switch_box) 54 55self.edit_mode_switch.connect("state-set", self.set_edit_mode) 56 57self.set_title("Panel configuration") 58 59self.connect("close-request", self.on_destroy) 60 61def on_destroy(self, widget): 62global manager_window 63print("Destroyed") 64self.destroy() 65manager_window = None 66 67def set_edit_mode(self, switch, value): 68print(f"Editing is {value}") 69 70if value: 71for panel in panels: 72panel.set_edit_mode(True) 73else: 74for panel in panels: 75panel.set_edit_mode(False) 76 77 78manager_window = None 79 80 81class AppletArea(Gtk.Box): 82def __init__(self, orientation=Gtk.Orientation.HORIZONTAL): 83super().__init__() 84 85def set_edit_mode(self, value): 86child = self.get_first_child() 87while child is not None: 88child.set_sensitive(not value) 89child.set_opacity(0.75 if value else 1) 90child = child.get_next_sibling() 91 92 93class Panel(Gtk.Window): 94def __init__(self, monitor: Gdk.Monitor, position: Gtk.PositionType = Gtk.PositionType.TOP, size: int = 40): 95super().__init__() 96self.set_default_size(800, size) 97self.set_decorated(False) 98 99Gtk4LayerShell.init_for_window(self) 100 101Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP) 102 103Gtk4LayerShell.auto_exclusive_zone_enable(self) 104Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True) 105Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True) 106Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True) 107 108box = Gtk.CenterBox() 109 110match position: 111case Gtk.PositionType.TOP | Gtk.PositionType.BOTTOM: 112box.set_orientation(Gtk.Orientation.HORIZONTAL) 113case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT: 114box.set_orientation(Gtk.Orientation.VERTICAL) 115 116self.set_child(box) 117 118self.left_area = AppletArea(orientation=box.get_orientation()) 119self.centre_area = AppletArea(orientation=box.get_orientation()) 120self.right_area = AppletArea(orientation=box.get_orientation()) 121 122box.set_start_widget(self.left_area) 123box.set_center_widget(self.centre_area) 124box.set_end_widget(self.right_area) 125 126# Add a context menu 127menu = Gio.Menu() 128 129menu.append("Open _manager", "panel.manager") 130 131self.context_menu = Gtk.PopoverMenu.new_from_model(menu) 132self.context_menu.set_has_arrow(False) 133self.context_menu.set_parent(self) 134self.context_menu.set_halign(Gtk.Align.START) 135self.context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 136 137right_click_controller = Gtk.GestureClick() 138right_click_controller.set_button(3) 139right_click_controller.connect("pressed", self.show_context_menu) 140 141self.add_controller(right_click_controller) 142 143action_group = Gio.SimpleActionGroup() 144manager_action = Gio.SimpleAction.new("manager", None) 145manager_action.connect("activate", self.show_manager) 146action_group.add_action(manager_action) 147self.insert_action_group("panel", action_group) 148 149def set_edit_mode(self, value): 150for area in (self.left_area, self.centre_area, self.right_area): 151area.set_edit_mode(value) 152 153def show_context_menu(self, gesture, n_presses, x, y): 154rect = Gdk.Rectangle() 155rect.x = int(x) 156rect.y = int(y) 157rect.width = 1 158rect.height = 1 159 160self.context_menu.set_pointing_to(rect) 161self.context_menu.popup() 162 163def show_manager(self, _0=None, _1=None): 164print("Showing manager") 165global manager_window 166if not manager_window: 167manager_window = ManagerWindow() 168manager_window.present() 169 170def get_orientation(self): 171box = self.get_first_child() 172return box.get_orientation() 173 174 175display = Gdk.Display.get_default() 176monitors = display.get_monitors() 177 178for i, monitor in enumerate(monitors): 179geometry = monitor.get_geometry() 180print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}") 181 182 183def get_all_subclasses(klass: type) -> list[type]: 184subclasses = [] 185for subclass in klass.__subclasses__(): 186subclasses.append(subclass) 187subclasses += get_all_subclasses(subclass) 188 189return subclasses 190 191 192def load_packages_from_dir(dir_path: Path): 193loaded_modules = [] 194 195for path in dir_path.iterdir(): 196if path.name.startswith("_"): 197continue 198 199if path.is_dir() and (path / "__init__.py").exists(): 200module_name = path.name 201spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py") 202module = importlib.util.module_from_spec(spec) 203spec.loader.exec_module(module) 204loaded_modules.append(module) 205else: 206continue 207 208return loaded_modules 209 210 211all_applets = list(chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories())) 212 213 214print("Applets:") 215subclasses = get_all_subclasses(panorama_panel.Applet) 216applets_by_name = {} 217for subclass in subclasses: 218if subclass.__name__ in applets_by_name: 219print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.", file=sys.stderr) 220applets_by_name[subclass.__name__] = subclass 221 222 223with open(get_config_file(), "r") as config_file: 224yaml_loader = yaml.YAML(typ="rt") 225yaml_file = yaml_loader.load(config_file) 226for panel_data in yaml_file["panels"]: 227position = panel_data["position"] 228monitor_index = panel_data["monitor"] 229size = panel_data["size"] 230 231panel = Panel(position, monitor_index, size) 232panels.append(panel) 233panel.show() 234 235print(f"{size}px panel on {position} edge of monitor {monitor_index}") 236 237for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)): 238applet_list = panel_data["applets"].get(area_name) 239if applet_list is None: 240continue 241 242for applet in applet_list: 243item = list(applet.items())[0] 244AppletClass = applets_by_name[item[0]] 245options = item[1] 246 247applet_widget = AppletClass(orientation=panel.get_orientation(), config=options) 248 249area.append(applet_widget) 250 251 252loop = GLib.MainLoop() 253loop.run() 254