from gi.repository import GObject from sdbus import ( DbusInterfaceCommonAsync, dbus_method_async, dbus_property_async, dbus_signal_async, ) from .config import WorkspaceConfig, GroupConfig, Context as ContextConfig, Config from .backend import getBackend, WorkspaceAdapter, OutputAdapter from . import scoring import logging logger = logging.getLogger(__name__) class Workspace( GObject.Object, ): class DBusInterface( DbusInterfaceCommonAsync, interface_name="dev.ezri.voidshell.workspaces.Workspace", ): def __init__(self, workspace: "Workspace" = None): super().__init__() self.workspace = workspace @dbus_signal_async() def Focused(self): raise NotImplementedError @dbus_signal_async() def LostFocus(self): raise NotImplementedError @dbus_signal_async() def Created(self): raise NotImplementedError @dbus_signal_async() def Destroyed(self): raise NotImplementedError @dbus_signal_async("oo") def Moved(self, oldGroupPath: str, newGroupPath: str): raise NotImplementedError @dbus_signal_async() def Alert(self): raise NotImplementedError @dbus_method_async(input_signature="", result_signature="s") async def Focus(self): await self.workspace.focus() @dbus_method_async(input_signature="o", result_signature="s") async def Reassign(self, groupPath: str): await self.workspace.reassign(Manager.get_manager().pathToObject(groupPath)) @dbus_property_async("s") def Name(self): return self.workspace.definition["name"] @dbus_property_async("s") def Output(self): return self.workspace.current_output @dbus_property_async("o") def CurrentGroup(self): return self.workspace.current_group.bus_name @dbus_property_async("ao") def Groups(self): return [group.bus_name for group in self.workspace.groups] @dbus_property_async("b") def IsFocused(self): return self.workspace.focused @dbus_property_async("b") def IsVisible(self): return self.workspace.visible @dbus_property_async("b") def IsActive(self): return self.workspace.active @dbus_property_async("b") def IsAlerted(self): return self.workspace.alerted class DBusObject(DBusInterface): def __init__(self, workspace: "Workspace"): super().__init__(workspace) self.export_to_dbus(workspace.bus_name) class DBusProxy(DBusInterface): def __init__(self, workspaceIndex: str | int): super().__init__() if workspaceIndex is int: self._proxify( "dev.ezri.voidshell", f"/dev/ezri/voidshell/workspaces/workspace/{workspaceIndex}", ) else: self._proxify( "dev.ezri.voidshell", workspaceIndex, ) def __init__( self, config: WorkspaceConfig, ): super().__init__() self.definition = config self.groups = [] self._current_group = None self._current_output = None self._focused = False self._visible = False self._active = False self._alerted = False self._dbusObject = Workspace.DBusObject(self) @GObject.Property def current_group(self): return self._current_group @GObject.Property def current_output(self): return self._current_output @GObject.Property def focused(self): return self._focused @GObject.Property def visible(self): return self._visible @GObject.Property def active(self): return self._active @GObject.Property def alerted(self): return self._alerted @GObject.Property def bus_name(self): return f"/dev/ezri/voidshell/workspaces/workspace/{self.definition['index']}" async def focus(self): """Focus the workspace""" adapter = getBackend() await adapter.focusWorkspace(self.definition["index"]) async def reassign(self, group): """Reassign the workspace to a different group""" adapter = getBackend() await adapter.moveWorkspace(self.definition["index"], group.output_name) def sync(self, data: WorkspaceAdapter): """Synchronize the state of the workspace object with data from the compositor.""" if not self._active: self._active = True self._dbusObject.Created.emit(None) manager = Manager.get_manager() newOutput = data.output_name if newOutput != self._current_output: self._current_output = newOutput self.notify("current-output") newGroup = manager.getGroupByOutput(newOutput) if newGroup != self._current_group: oldGroup = self._current_group self._current_group = newGroup self.notify("current-group") if newGroup not in self.groups: logger.warn( f"Group {newGroup} not in workspace {self.definition['name']}" ) self._dbusObject.Moved.emit( oldGroup.bus_name, newGroup.bus_name, ) newGroup.notify("focused-workspace") if data.focused != self._focused: self._focused = data.focused self.notify("focused") if data.focused: self._dbusObject.Focused.emit(None) else: self._dbusObject.LostFocus.emit(None) if data.visible != self._visible: self._visible = data.visible self.notify("visible") if data.urgent != self._alerted: self._alerted = data.urgent self.notify("alerted") def deactivate(self): """Deactivate the workspace.""" if self._focused: self._focused = False self.notify("focused") self._dbusObject.LostFocus.emit(None) if self._active: self._active = False self.notify("active") self._dbusObject.Destroyed.emit(None) if self._visible: self._visible = False self.notify("visible") if self._alerted: self._alerted = False self.notify("alerted") def addGroup(self, group: "Group"): self.groups.append(group) class Group(GObject.Object): class DBusInterface( DbusInterfaceCommonAsync, interface_name="dev.ezri.voidshell.workspaces.Group", ): def __init__(self, group: "Group" = None): super().__init__() self.group = group @dbus_signal_async("") def Focused(self): raise NotImplementedError @dbus_signal_async("") def LostFocus(self): raise NotImplementedError @dbus_signal_async("o") def VisibleWorkspaceChanged(self, workspacePath: str): raise NotImplementedError @dbus_method_async(input_signature="", result_signature="s") async def Focus(self): return await self.group.focus() @dbus_method_async(input_signature="", result_signature="s") async def Reconfigure(self): return await self.group.configure() @dbus_property_async("s") def Name(self): return self.group.definition["name"] @dbus_property_async("s") def Output(self): return self.group.output_name @dbus_property_async("b") def IsFocused(self): return self.group.focused @dbus_property_async("o") def Context(self): return self.group.context.bus_name @dbus_property_async("o") def FocusedWorkspace(self): return self.group.focused_workspace.bus_name @dbus_property_async("ao") def Workspaces(self): return [w.bus_name for w in self.group.workspaces] class DBusObject(DBusInterface): def __init__(self, group: "Group"): super().__init__(group) self.export_to_dbus(group.bus_name) class DBusProxy(DBusInterface): def __init__(self, name: str): super().__init__() if "/" in name: self._proxify("dev.ezri.voidshell", name) else: self._proxify( "dev.ezri.voidshell", f"/dev/ezri/voidshell/workspaces/group/{name}" ) def __init__(self, definition: GroupConfig): super().__init__() manager = Manager.get_manager() self.definition = definition self.workspaces = [ manager._getWorkspaceByCompositorIndex(ws) for ws in definition.get("workspaces") ] for workspace in self.workspaces: workspace.addGroup(self) self.output_name = None self.focused = False self._focused_workspace = None self.context = None self._dbusObject = Group.DBusObject(self) async def configure(self, output: str = None) -> str: """Configure the group. If no output is provided, the group will be reconfigured on its current output. If it is not currently active, an output is required.""" adapter = getBackend() if output is None: if self.output_name is None: return "Output required for inactive group" output = self.output_name self.output_name = output try: await adapter.configureOutput(output, self.definition) except Exception as e: self.output_name = None return str(e) return "OK" async def focus(self): """Focus the group.""" adapter = getBackend() if self.output_name is None: return "Group not active, cannot focus" try: await adapter.focusOutput(self.output_name) except Exception as e: return str(e) return "OK" async def disable(self): """Disable the group.""" adapter = getBackend() if self.output_name is None: return "Don't have an output name, cannot disable group." try: await adapter.disableOutput(self.output_name) except Exception as e: return str(e) return "OK" class Context( GObject.Object, ): class DBusInterface( DbusInterfaceCommonAsync, interface_name="dev.ezri.voidshell.workspaces.Context", ): def __init__(self, context: "Context" = None): super().__init__() self.context = context @dbus_signal_async() async def Activated(self): raise NotImplementedError @dbus_signal_async() async def Deactivated(self): raise NotImplementedError @dbus_method_async(input_signature="", result_signature="i") async def GetCompatabilityScore(self): raise NotImplementedError @dbus_property_async("b") def Active(self): raise NotImplementedError @dbus_property_async("ao") def Groups(self): raise NotImplementedError class DBusObject(DBusInterface): def __init__(self, context: "Context"): super().__init__(context) self.export_to_dbus( f"/dev/ezri/voidshell/workspaces/context/{context.name}" ) class DBusProxy(DBusInterface): def __init__(self, name: str): super().__init__() if "/" in name: self._proxify("dev.ezri.voidshell", name) else: self._proxify( "dev.ezri.voidshell", f"/dev/ezri/voidshell/workspaces/context/{name}", ) def __init__(self, definition: ContextConfig): super().__init__() self.definition = definition self.name = definition["name"] self.groups = {group["name"]: Group(group) for group in definition["groups"]} self.priority = int(self.definition.get("priority", 0)) async def score(self, outputs: list[OutputAdapter] = None): if outputs is None: adapter = getBackend() outputs = await adapter.getOutputs() return scoring.computePerfectScore(self.definition, outputs) async def activate(self): pass class Manager( GObject.Object, ): class DBusInterface( DbusInterfaceCommonAsync, interface_name="dev.ezri.voidshell.workspaces.Tree", ): def __init__(self, tree: "Manager" = None): super().__init__() self.tree = tree @dbus_signal_async() async def Updated(self): raise NotImplementedError @dbus_method_async(input_signature="s", result_signature="s") async def GetWorkspace(self, workspacePath: str): return self.tree.getWorkspace(workspacePath) class DBusObject(DBusInterface): def __init__(self, tree: "Manager"): super().__init__(tree) self.export_to_dbus("/dev/ezri/voidshell/workspaces/tree") class DBusProxy(DBusInterface): def __init__(self): super().__init__() self._proxify("dev.ezri.voidshell", "/dev/ezri/voidshell/workspaces/tree") _instance = None @staticmethod def get_manager() -> "Manager": return Manager._instance def __init__( self, config: Config, ): super().__init__() Manager._instance = self self.config = config self.workspaces = [ Workspace(workspaceConfig) for workspaceConfig in self.config.workspaces ] def pathToObject(self, path: str): # Path structure: /dev/ezri/voidshell/workspaces/{context,group,workspace}/{index for workspace, name for others} path = path.split("/") if ( path[0] != "" or path[1] != "dev" or path[2] != "ezri" or path[3] != "voidshell" or path[4] != "workspaces" ): raise ValueError("Invalid path") if path[5] == "context": return self.findContextByName(path[6]) if path[5] == "group": return self.findGroupByName(path[6]) if path[5] == "workspace": return self.findWorkspaceByIndex(int(path[6])) raise ValueError("Invalid path") def getGroupByOutput(self, outputName: str): """Get the group that is currently assigned to the given output""" pass