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 63self.destroy() 64manager_window = None 65 66def set_edit_mode(self, switch, value): 67print(f"Editing is {value}") 68 69if value: 70for panel in panels: 71panel.set_edit_mode(True) 72else: 73for panel in panels: 74panel.set_edit_mode(False) 75 76 77manager_window = None 78 79 80class AppletArea(Gtk.Box): 81def __init__(self, orientation=Gtk.Orientation.HORIZONTAL): 82super().__init__() 83 84def set_edit_mode(self, value): 85child = self.get_first_child() 86while child is not None: 87child.set_sensitive(not value) 88child.set_opacity(0.75 if value else 1) 89child = child.get_next_sibling() 90 91 92class Panel(Gtk.Window): 93def __init__(self, monitor: Gdk.Monitor, position: Gtk.PositionType = Gtk.PositionType.TOP, size: int = 40): 94super().__init__() 95self.set_default_size(800, size) 96self.set_decorated(False) 97 98Gtk4LayerShell.init_for_window(self) 99 100Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP) 101 102Gtk4LayerShell.auto_exclusive_zone_enable(self) 103Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True) 104Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True) 105Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True) 106 107box = Gtk.CenterBox() 108 109match position: 110case Gtk.PositionType.TOP | Gtk.PositionType.BOTTOM: 111box.set_orientation(Gtk.Orientation.HORIZONTAL) 112case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT: 113box.set_orientation(Gtk.Orientation.VERTICAL) 114 115self.set_child(box) 116 117self.left_area = AppletArea(orientation=box.get_orientation()) 118self.centre_area = AppletArea(orientation=box.get_orientation()) 119self.right_area = AppletArea(orientation=box.get_orientation()) 120 121box.set_start_widget(self.left_area) 122box.set_center_widget(self.centre_area) 123box.set_end_widget(self.right_area) 124 125# Add a context menu 126menu = Gio.Menu() 127 128menu.append("Open _manager", "panel.manager") 129 130self.context_menu = Gtk.PopoverMenu.new_from_model(menu) 131self.context_menu.set_has_arrow(False) 132self.context_menu.set_parent(self) 133self.context_menu.set_halign(Gtk.Align.START) 134self.context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 135 136right_click_controller = Gtk.GestureClick() 137right_click_controller.set_button(3) 138right_click_controller.connect("pressed", self.show_context_menu) 139 140self.add_controller(right_click_controller) 141 142action_group = Gio.SimpleActionGroup() 143manager_action = Gio.SimpleAction.new("manager", None) 144manager_action.connect("activate", self.show_manager) 145action_group.add_action(manager_action) 146self.insert_action_group("panel", action_group) 147 148def set_edit_mode(self, value): 149for area in (self.left_area, self.centre_area, self.right_area): 150area.set_edit_mode(value) 151 152def show_context_menu(self, gesture, n_presses, x, y): 153rect = Gdk.Rectangle() 154rect.x = int(x) 155rect.y = int(y) 156rect.width = 1 157rect.height = 1 158 159self.context_menu.set_pointing_to(rect) 160self.context_menu.popup() 161 162def show_manager(self, _0=None, _1=None): 163print("Showing manager") 164global manager_window 165if not manager_window: 166manager_window = ManagerWindow() 167manager_window.present() 168 169def get_orientation(self): 170box = self.get_first_child() 171return box.get_orientation() 172 173 174display = Gdk.Display.get_default() 175monitors = display.get_monitors() 176 177for i, monitor in enumerate(monitors): 178geometry = monitor.get_geometry() 179print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}") 180 181 182def get_all_subclasses(klass: type) -> list[type]: 183subclasses = [] 184for subclass in klass.__subclasses__(): 185subclasses.append(subclass) 186subclasses += get_all_subclasses(subclass) 187 188return subclasses 189 190 191def load_packages_from_dir(dir_path: Path): 192loaded_modules = [] 193 194for path in dir_path.iterdir(): 195if path.name.startswith("_"): 196continue 197 198if path.is_dir() and (path / "__init__.py").exists(): 199module_name = path.name 200spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py") 201module = importlib.util.module_from_spec(spec) 202spec.loader.exec_module(module) 203loaded_modules.append(module) 204else: 205continue 206 207return loaded_modules 208 209 210all_applets = list(chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories())) 211 212 213print("Applets:") 214subclasses = get_all_subclasses(panorama_panel.Applet) 215applets_by_name = {} 216for subclass in subclasses: 217if subclass.__name__ in applets_by_name: 218print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.", file=sys.stderr) 219applets_by_name[subclass.__name__] = subclass 220 221 222PANEL_POSITIONS = { 223"top": Gtk.PositionType.TOP, 224"bottom": Gtk.PositionType.BOTTOM, 225"left": Gtk.PositionType.LEFT, 226"right": Gtk.PositionType.RIGHT, 227} 228 229 230with open(get_config_file(), "r") as config_file: 231yaml_loader = yaml.YAML(typ="rt") 232yaml_file = yaml_loader.load(config_file) 233for panel_data in yaml_file["panels"]: 234position = PANEL_POSITIONS[panel_data["position"]] 235monitor_index = panel_data["monitor"] 236monitor = monitors[monitor_index] 237size = panel_data["size"] 238 239panel = Panel(monitor, position, size) 240panels.append(panel) 241panel.show() 242 243print(f"{size}px panel on {position} edge of monitor {monitor_index}") 244 245for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)): 246applet_list = panel_data["applets"].get(area_name) 247if applet_list is None: 248continue 249 250for applet in applet_list: 251item = list(applet.items())[0] 252AppletClass = applets_by_name[item[0]] 253options = item[1] 254applet_widget = AppletClass(orientation=panel.get_orientation(), config=options) 255 256area.append(applet_widget) 257 258 259loop = GLib.MainLoop() 260loop.run() 261