From 8ecec8768dfd6e760b7b82d0f210128d8f51854f Mon Sep 17 00:00:00 2001 From: Makesesama Date: Mon, 19 May 2025 09:32:17 +0200 Subject: [PATCH] squash --- bar/main.py | 16 +- bar/modules/bar.py | 16 +- bar/modules/window_fuzzy.py | 23 +- bar/services/__init__.py | 0 bar/services/river/__init__.py | 3 + bar/services/river/protocols/__init__.py | 0 .../river/protocols/generated/__init__.py | 0 .../river_control_unstable_v1/__init__.py | 18 + .../zriver_command_callback_v1.py | 84 +++++ .../zriver_control_v1.py | 111 ++++++ .../river_status_unstable_v1/__init__.py | 19 + .../zriver_output_status_v1.py | 134 +++++++ .../zriver_seat_status_v1.py | 124 ++++++ .../zriver_status_manager_v1.py | 102 +++++ .../protocols/river-control-unstable-v1.xml | 85 +++++ .../protocols/river-status-unstable-v1.xml | 148 ++++++++ bar/services/river/service.py | 352 ++++++++++++++++++ bar/services/river/widgets.py | 266 +++++++++++++ bar/services/wlr/event_loop.py | 21 ++ bar/services/wlr/protocol/windows.py | 233 ++++++++++++ ...oreign-toplevel-management-unstable-v1.xml | 270 ++++++++++++++ .../__init__.py | 27 ++ .../zwlr_foreign_toplevel_handle_v1.py | 352 ++++++++++++++++++ .../zwlr_foreign_toplevel_manager_v1.py | 112 ++++++ bar/services/wlr/service.py | 238 ++++++++++++ flake.lock | 10 +- flake.nix | 16 +- 27 files changed, 2752 insertions(+), 28 deletions(-) create mode 100644 bar/services/__init__.py create mode 100644 bar/services/river/__init__.py create mode 100644 bar/services/river/protocols/__init__.py create mode 100644 bar/services/river/protocols/generated/__init__.py create mode 100644 bar/services/river/protocols/generated/river_control_unstable_v1/__init__.py create mode 100644 bar/services/river/protocols/generated/river_control_unstable_v1/zriver_command_callback_v1.py create mode 100644 bar/services/river/protocols/generated/river_control_unstable_v1/zriver_control_v1.py create mode 100644 bar/services/river/protocols/generated/river_status_unstable_v1/__init__.py create mode 100644 bar/services/river/protocols/generated/river_status_unstable_v1/zriver_output_status_v1.py create mode 100644 bar/services/river/protocols/generated/river_status_unstable_v1/zriver_seat_status_v1.py create mode 100644 bar/services/river/protocols/generated/river_status_unstable_v1/zriver_status_manager_v1.py create mode 100644 bar/services/river/protocols/river-control-unstable-v1.xml create mode 100644 bar/services/river/protocols/river-status-unstable-v1.xml create mode 100644 bar/services/river/service.py create mode 100644 bar/services/river/widgets.py create mode 100644 bar/services/wlr/event_loop.py create mode 100644 bar/services/wlr/protocol/windows.py create mode 100644 bar/services/wlr/protocol/wlr-foreign-toplevel-management-unstable-v1.xml create mode 100644 bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/__init__.py create mode 100644 bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/zwlr_foreign_toplevel_handle_v1.py create mode 100644 bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/zwlr_foreign_toplevel_manager_v1.py create mode 100644 bar/services/wlr/service.py diff --git a/bar/main.py b/bar/main.py index 79df1cf..2ce1381 100644 --- a/bar/main.py +++ b/bar/main.py @@ -3,17 +3,18 @@ from loguru import logger from fabric import Application from fabric.system_tray.widgets import SystemTray from fabric.widgets.wayland import WaylandWindow as Window -from fabric.river.widgets import ( - get_river_connection, -) + + from fabric.utils import ( get_relative_path, ) from .modules.bar import StatusBar from .modules.window_fuzzy import FuzzyWindowFinder - +from .services.river.widgets import get_river_connection +from .services.wlr.event_loop import WaylandEventLoopService tray = SystemTray(name="system-tray", spacing=4) +wayland_event_loop = WaylandEventLoopService() river = get_river_connection() dummy = Window(visible=False) @@ -36,7 +37,12 @@ def spawn_bars(): output_ids = sorted(outputs.keys()) for i, output_id in enumerate(output_ids): - bar = StatusBar(display=output_id, tray=tray if i == 0 else None, monitor=i) + bar = StatusBar( + display=output_id, + tray=tray if i == 0 else None, + monitor=i, + river_service=river, + ) bar_windows.append(bar) return False diff --git a/bar/modules/bar.py b/bar/modules/bar.py index 5a071e2..6c70152 100644 --- a/bar/modules/bar.py +++ b/bar/modules/bar.py @@ -8,18 +8,19 @@ from bar.modules.player import Player from bar.modules.vinyl import VinylButton from fabric.widgets.wayland import WaylandWindow as Window from fabric.system_tray.widgets import SystemTray -from fabric.river.widgets import ( - RiverWorkspaces, - RiverWorkspaceButton, - RiverActiveWindow, - get_river_connection, -) + from fabric.utils import ( invoke_repeater, ) from fabric.widgets.circularprogressbar import CircularProgressBar from bar.config import VINYL +from bar.services.river.widgets import ( + RiverWorkspaces, + RiverWorkspaceButton, + RiverActiveWindow, + get_river_connection, +) class StatusBar(Window): @@ -42,8 +43,6 @@ class StatusBar(Window): ) if river_service: self.river = river_service - else: - self.river = get_river_connection() self.workspaces = RiverWorkspaces( display, @@ -56,6 +55,7 @@ class StatusBar(Window): self.system_tray = tray self.active_window = RiverActiveWindow( + river_service=self.river, name="active-window", max_length=50, style="color: #ffffff; font-size: 14px; font-weight: bold;", diff --git a/bar/modules/window_fuzzy.py b/bar/modules/window_fuzzy.py index 20dfe12..eebe699 100644 --- a/bar/modules/window_fuzzy.py +++ b/bar/modules/window_fuzzy.py @@ -3,8 +3,9 @@ from fabric.widgets.wayland import WaylandWindow as Window from fabric.widgets.box import Box from fabric.widgets.label import Label from fabric.widgets.entry import Entry -from fabric.utils import idle_add from gi.repository import Gdk +from bar.services.wlr.service import WaylandWindowTracker, Window as WaylandWindow +from pywayland.client import Display class FuzzyWindowFinder(Window): @@ -20,8 +21,9 @@ class FuzzyWindowFinder(Window): type="popup", visible=False, ) - - self._all_windows = ["Test", "Uwu", "Tidal"] + self.window_tracker = WaylandWindowTracker() + self.window_tracker.ready_signal.connect(lambda *_: print("Tracker is ready")) + self._all_windows: list[WaylandWindow] = [] self.viewport = Box(name="viewport", spacing=4, orientation="v") @@ -46,6 +48,12 @@ class FuzzyWindowFinder(Window): self.add(self.picker_box) self.arrange_viewport("") + def open(self): + self._all_windows = self.window_tracker.windows + print(self._all_windows[0]) + self.arrange_viewport("") + self.show() + def notify_text(self, entry, *_): text = entry.get_text() self.arrange_viewport(text) # Update list on typing @@ -56,6 +64,8 @@ class FuzzyWindowFinder(Window): # self.move_selection_2d(event.keyval) # return True print(event.keyval) + if event.keyval == Gdk.KEY_Return: + self.window_tracker.activate_window(self._filtered[0]) if event.keyval in [Gdk.KEY_Escape, 103]: self.hide() return True @@ -67,9 +77,12 @@ class FuzzyWindowFinder(Window): def arrange_viewport(self, query: str = ""): self.viewport.children = [] # Clear previous entries - filtered = [w for w in self._all_windows if query.lower() in w.lower()] + self._filtered = [ + w for w in self._all_windows if query.lower() in w.title.lower() + ] + titles = [w.title for w in self._filtered] - for window in filtered: + for window in titles: self.viewport.add( Box(name="slot-box", orientation="h", children=[Label(label=window)]) ) diff --git a/bar/services/__init__.py b/bar/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bar/services/river/__init__.py b/bar/services/river/__init__.py new file mode 100644 index 0000000..84c4286 --- /dev/null +++ b/bar/services/river/__init__.py @@ -0,0 +1,3 @@ +from .service import River, RiverEvent + +__all__ = ["River", "RiverEvent"] diff --git a/bar/services/river/protocols/__init__.py b/bar/services/river/protocols/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bar/services/river/protocols/generated/__init__.py b/bar/services/river/protocols/generated/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bar/services/river/protocols/generated/river_control_unstable_v1/__init__.py b/bar/services/river/protocols/generated/river_control_unstable_v1/__init__.py new file mode 100644 index 0000000..3801d2e --- /dev/null +++ b/bar/services/river/protocols/generated/river_control_unstable_v1/__init__.py @@ -0,0 +1,18 @@ +# This file has been autogenerated by the pywayland scanner + +# Copyright 2020 The River Developers +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from .zriver_command_callback_v1 import ZriverCommandCallbackV1 # noqa: F401 +from .zriver_control_v1 import ZriverControlV1 # noqa: F401 diff --git a/bar/services/river/protocols/generated/river_control_unstable_v1/zriver_command_callback_v1.py b/bar/services/river/protocols/generated/river_control_unstable_v1/zriver_command_callback_v1.py new file mode 100644 index 0000000..a6c264d --- /dev/null +++ b/bar/services/river/protocols/generated/river_control_unstable_v1/zriver_command_callback_v1.py @@ -0,0 +1,84 @@ +# This file has been autogenerated by the pywayland scanner + +# Copyright 2020 The River Developers +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import annotations + +from pywayland.protocol_core import (Argument, ArgumentType, Global, Interface, + Proxy, Resource) + + +class ZriverCommandCallbackV1(Interface): + """Callback object + + This object is created by the run_command request. Exactly one of the + success or failure events will be sent. This object will be destroyed by + the compositor after one of the events is sent. + """ + + name = "zriver_command_callback_v1" + version = 1 + + +class ZriverCommandCallbackV1Proxy(Proxy[ZriverCommandCallbackV1]): + interface = ZriverCommandCallbackV1 + + +class ZriverCommandCallbackV1Resource(Resource): + interface = ZriverCommandCallbackV1 + + @ZriverCommandCallbackV1.event( + Argument(ArgumentType.String), + ) + def success(self, output: str) -> None: + """Command successful + + Sent when the command has been successfully received and executed by + the compositor. Some commands may produce output, in which case the + output argument will be a non-empty string. + + :param output: + the output of the command + :type output: + `ArgumentType.String` + """ + self._post_event(0, output) + + @ZriverCommandCallbackV1.event( + Argument(ArgumentType.String), + ) + def failure(self, failure_message: str) -> None: + """Command failed + + Sent when the command could not be carried out. This could be due to + sending a non-existent command, no command, not enough arguments, too + many arguments, invalid arguments, etc. + + :param failure_message: + a message explaining why failure occurred + :type failure_message: + `ArgumentType.String` + """ + self._post_event(1, failure_message) + + +class ZriverCommandCallbackV1Global(Global): + interface = ZriverCommandCallbackV1 + + +ZriverCommandCallbackV1._gen_c() +ZriverCommandCallbackV1.proxy_class = ZriverCommandCallbackV1Proxy +ZriverCommandCallbackV1.resource_class = ZriverCommandCallbackV1Resource +ZriverCommandCallbackV1.global_class = ZriverCommandCallbackV1Global diff --git a/bar/services/river/protocols/generated/river_control_unstable_v1/zriver_control_v1.py b/bar/services/river/protocols/generated/river_control_unstable_v1/zriver_control_v1.py new file mode 100644 index 0000000..567c5e3 --- /dev/null +++ b/bar/services/river/protocols/generated/river_control_unstable_v1/zriver_control_v1.py @@ -0,0 +1,111 @@ +# This file has been autogenerated by the pywayland scanner + +# Copyright 2020 The River Developers +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import annotations + +from pywayland.protocol_core import ( + Argument, + ArgumentType, + Global, + Interface, + Proxy, + Resource, +) + +from pywayland.protocol.wayland import WlSeat +from .zriver_command_callback_v1 import ZriverCommandCallbackV1 + + +class ZriverControlV1(Interface): + """Run compositor commands + + This interface allows clients to run compositor commands and receive a + success/failure response with output or a failure message respectively. + + Each command is built up in a series of add_argument requests and executed + with a run_command request. The first argument is the command to be run. + + A complete list of commands should be made available in the man page of the + compositor. + """ + + name = "zriver_control_v1" + version = 1 + + +class ZriverControlV1Proxy(Proxy[ZriverControlV1]): + interface = ZriverControlV1 + + @ZriverControlV1.request() + def destroy(self) -> None: + """Destroy the river_control object + + This request indicates that the client will not use the river_control + object any more. Objects that have been created through this instance + are not affected. + """ + self._marshal(0) + self._destroy() + + @ZriverControlV1.request( + Argument(ArgumentType.String), + ) + def add_argument(self, argument: str) -> None: + """Add an argument to the current command + + Arguments are stored by the server in the order they were sent until + the run_command request is made. + + :param argument: + the argument to add + :type argument: + `ArgumentType.String` + """ + self._marshal(1, argument) + + @ZriverControlV1.request( + Argument(ArgumentType.Object, interface=WlSeat), + Argument(ArgumentType.NewId, interface=ZriverCommandCallbackV1), + ) + def run_command(self, seat: WlSeat) -> Proxy[ZriverCommandCallbackV1]: + """Run the current command + + Execute the command built up using the add_argument request for the + given seat. + + :param seat: + :type seat: + :class:`~pywayland.protocol.wayland.WlSeat` + :returns: + :class:`~pywayland.protocol.river_control_unstable_v1.ZriverCommandCallbackV1` + -- callback object + """ + callback = self._marshal_constructor(2, ZriverCommandCallbackV1, seat) + return callback + + +class ZriverControlV1Resource(Resource): + interface = ZriverControlV1 + + +class ZriverControlV1Global(Global): + interface = ZriverControlV1 + + +ZriverControlV1._gen_c() +ZriverControlV1.proxy_class = ZriverControlV1Proxy +ZriverControlV1.resource_class = ZriverControlV1Resource +ZriverControlV1.global_class = ZriverControlV1Global diff --git a/bar/services/river/protocols/generated/river_status_unstable_v1/__init__.py b/bar/services/river/protocols/generated/river_status_unstable_v1/__init__.py new file mode 100644 index 0000000..51d6f86 --- /dev/null +++ b/bar/services/river/protocols/generated/river_status_unstable_v1/__init__.py @@ -0,0 +1,19 @@ +# This file has been autogenerated by the pywayland scanner + +# Copyright 2020 The River Developers +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from .zriver_output_status_v1 import ZriverOutputStatusV1 # noqa: F401 +from .zriver_seat_status_v1 import ZriverSeatStatusV1 # noqa: F401 +from .zriver_status_manager_v1 import ZriverStatusManagerV1 # noqa: F401 diff --git a/bar/services/river/protocols/generated/river_status_unstable_v1/zriver_output_status_v1.py b/bar/services/river/protocols/generated/river_status_unstable_v1/zriver_output_status_v1.py new file mode 100644 index 0000000..6cc53ce --- /dev/null +++ b/bar/services/river/protocols/generated/river_status_unstable_v1/zriver_output_status_v1.py @@ -0,0 +1,134 @@ +# This file has been autogenerated by the pywayland scanner + +# Copyright 2020 The River Developers +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import annotations + +from pywayland.protocol_core import (Argument, ArgumentType, Global, Interface, + Proxy, Resource) + + +class ZriverOutputStatusV1(Interface): + """Track output tags and focus + + This interface allows clients to receive information about the current + windowing state of an output. + """ + + name = "zriver_output_status_v1" + version = 4 + + +class ZriverOutputStatusV1Proxy(Proxy[ZriverOutputStatusV1]): + interface = ZriverOutputStatusV1 + + @ZriverOutputStatusV1.request() + def destroy(self) -> None: + """Destroy the river_output_status object + + This request indicates that the client will not use the + river_output_status object any more. + """ + self._marshal(0) + self._destroy() + + +class ZriverOutputStatusV1Resource(Resource): + interface = ZriverOutputStatusV1 + + @ZriverOutputStatusV1.event( + Argument(ArgumentType.Uint), + ) + def focused_tags(self, tags: int) -> None: + """Focused tags of the output + + Sent once binding the interface and again whenever the tag focus of the + output changes. + + :param tags: + 32-bit bitfield + :type tags: + `ArgumentType.Uint` + """ + self._post_event(0, tags) + + @ZriverOutputStatusV1.event( + Argument(ArgumentType.Array), + ) + def view_tags(self, tags: list) -> None: + """Tag state of an output's views + + Sent once on binding the interface and again whenever the tag state of + the output changes. + + :param tags: + array of 32-bit bitfields + :type tags: + `ArgumentType.Array` + """ + self._post_event(1, tags) + + @ZriverOutputStatusV1.event( + Argument(ArgumentType.Uint), + version=2, + ) + def urgent_tags(self, tags: int) -> None: + """Tags of the output with an urgent view + + Sent once on binding the interface and again whenever the set of tags + with at least one urgent view changes. + + :param tags: + 32-bit bitfield + :type tags: + `ArgumentType.Uint` + """ + self._post_event(2, tags) + + @ZriverOutputStatusV1.event( + Argument(ArgumentType.String), + version=4, + ) + def layout_name(self, name: str) -> None: + """Name of the layout + + Sent once on binding the interface should a layout name exist and again + whenever the name changes. + + :param name: + layout name + :type name: + `ArgumentType.String` + """ + self._post_event(3, name) + + @ZriverOutputStatusV1.event(version=4) + def layout_name_clear(self) -> None: + """Name of the layout + + Sent when the current layout name has been removed without a new one + being set, for example when the active layout generator disconnects. + """ + self._post_event(4) + + +class ZriverOutputStatusV1Global(Global): + interface = ZriverOutputStatusV1 + + +ZriverOutputStatusV1._gen_c() +ZriverOutputStatusV1.proxy_class = ZriverOutputStatusV1Proxy +ZriverOutputStatusV1.resource_class = ZriverOutputStatusV1Resource +ZriverOutputStatusV1.global_class = ZriverOutputStatusV1Global diff --git a/bar/services/river/protocols/generated/river_status_unstable_v1/zriver_seat_status_v1.py b/bar/services/river/protocols/generated/river_status_unstable_v1/zriver_seat_status_v1.py new file mode 100644 index 0000000..2931b80 --- /dev/null +++ b/bar/services/river/protocols/generated/river_status_unstable_v1/zriver_seat_status_v1.py @@ -0,0 +1,124 @@ +# This file has been autogenerated by the pywayland scanner + +# Copyright 2020 The River Developers +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import annotations + +from pywayland.protocol.wayland import WlOutput +from pywayland.protocol_core import (Argument, ArgumentType, Global, Interface, + Proxy, Resource) + + +class ZriverSeatStatusV1(Interface): + """Track seat focus + + This interface allows clients to receive information about the current + focus of a seat. Note that (un)focused_output events will only be sent if + the client has bound the relevant + :class:`~pywayland.protocol.wayland.WlOutput` globals. + """ + + name = "zriver_seat_status_v1" + version = 3 + + +class ZriverSeatStatusV1Proxy(Proxy[ZriverSeatStatusV1]): + interface = ZriverSeatStatusV1 + + @ZriverSeatStatusV1.request() + def destroy(self) -> None: + """Destroy the river_seat_status object + + This request indicates that the client will not use the + river_seat_status object any more. + """ + self._marshal(0) + self._destroy() + + +class ZriverSeatStatusV1Resource(Resource): + interface = ZriverSeatStatusV1 + + @ZriverSeatStatusV1.event( + Argument(ArgumentType.Object, interface=WlOutput), + ) + def focused_output(self, output: WlOutput) -> None: + """The seat focused an output + + Sent on binding the interface and again whenever an output gains focus. + + :param output: + :type output: + :class:`~pywayland.protocol.wayland.WlOutput` + """ + self._post_event(0, output) + + @ZriverSeatStatusV1.event( + Argument(ArgumentType.Object, interface=WlOutput), + ) + def unfocused_output(self, output: WlOutput) -> None: + """The seat unfocused an output + + Sent whenever an output loses focus. + + :param output: + :type output: + :class:`~pywayland.protocol.wayland.WlOutput` + """ + self._post_event(1, output) + + @ZriverSeatStatusV1.event( + Argument(ArgumentType.String), + ) + def focused_view(self, title: str) -> None: + """Information on the focused view + + Sent once on binding the interface and again whenever the focused view + or a property thereof changes. The title may be an empty string if no + view is focused or the focused view did not set a title. + + :param title: + title of the focused view + :type title: + `ArgumentType.String` + """ + self._post_event(2, title) + + @ZriverSeatStatusV1.event( + Argument(ArgumentType.String), + version=3, + ) + def mode(self, name: str) -> None: + """The active mode changed + + Sent once on binding the interface and again whenever a new mode is + entered (e.g. with riverctl enter-mode foobar). + + :param name: + name of the mode + :type name: + `ArgumentType.String` + """ + self._post_event(3, name) + + +class ZriverSeatStatusV1Global(Global): + interface = ZriverSeatStatusV1 + + +ZriverSeatStatusV1._gen_c() +ZriverSeatStatusV1.proxy_class = ZriverSeatStatusV1Proxy +ZriverSeatStatusV1.resource_class = ZriverSeatStatusV1Resource +ZriverSeatStatusV1.global_class = ZriverSeatStatusV1Global diff --git a/bar/services/river/protocols/generated/river_status_unstable_v1/zriver_status_manager_v1.py b/bar/services/river/protocols/generated/river_status_unstable_v1/zriver_status_manager_v1.py new file mode 100644 index 0000000..076450b --- /dev/null +++ b/bar/services/river/protocols/generated/river_status_unstable_v1/zriver_status_manager_v1.py @@ -0,0 +1,102 @@ +# This file has been autogenerated by the pywayland scanner + +# Copyright 2020 The River Developers +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import annotations + +from pywayland.protocol.wayland import WlOutput, WlSeat +from pywayland.protocol_core import (Argument, ArgumentType, Global, Interface, + Proxy, Resource) + +from .zriver_output_status_v1 import ZriverOutputStatusV1 +from .zriver_seat_status_v1 import ZriverSeatStatusV1 + + +class ZriverStatusManagerV1(Interface): + """Manage river status objects + + A global factory for objects that receive status information specific to + river. It could be used to implement, for example, a status bar. + """ + + name = "zriver_status_manager_v1" + version = 4 + + +class ZriverStatusManagerV1Proxy(Proxy[ZriverStatusManagerV1]): + interface = ZriverStatusManagerV1 + + @ZriverStatusManagerV1.request() + def destroy(self) -> None: + """Destroy the river_status_manager object + + This request indicates that the client will not use the + river_status_manager object any more. Objects that have been created + through this instance are not affected. + """ + self._marshal(0) + self._destroy() + + @ZriverStatusManagerV1.request( + Argument(ArgumentType.NewId, interface=ZriverOutputStatusV1), + Argument(ArgumentType.Object, interface=WlOutput), + ) + def get_river_output_status(self, output: WlOutput) -> Proxy[ZriverOutputStatusV1]: + """Create an output status object + + This creates a new river_output_status object for the given + :class:`~pywayland.protocol.wayland.WlOutput`. + + :param output: + :type output: + :class:`~pywayland.protocol.wayland.WlOutput` + :returns: + :class:`~pywayland.protocol.river_status_unstable_v1.ZriverOutputStatusV1` + """ + id = self._marshal_constructor(1, ZriverOutputStatusV1, output) + return id + + @ZriverStatusManagerV1.request( + Argument(ArgumentType.NewId, interface=ZriverSeatStatusV1), + Argument(ArgumentType.Object, interface=WlSeat), + ) + def get_river_seat_status(self, seat: WlSeat) -> Proxy[ZriverSeatStatusV1]: + """Create a seat status object + + This creates a new river_seat_status object for the given + :class:`~pywayland.protocol.wayland.WlSeat`. + + :param seat: + :type seat: + :class:`~pywayland.protocol.wayland.WlSeat` + :returns: + :class:`~pywayland.protocol.river_status_unstable_v1.ZriverSeatStatusV1` + """ + id = self._marshal_constructor(2, ZriverSeatStatusV1, seat) + return id + + +class ZriverStatusManagerV1Resource(Resource): + interface = ZriverStatusManagerV1 + + +class ZriverStatusManagerV1Global(Global): + interface = ZriverStatusManagerV1 + + +ZriverStatusManagerV1._gen_c() +ZriverStatusManagerV1.proxy_class = ZriverStatusManagerV1Proxy +ZriverStatusManagerV1.resource_class = ZriverStatusManagerV1Resource +ZriverStatusManagerV1.global_class = ZriverStatusManagerV1Global diff --git a/bar/services/river/protocols/river-control-unstable-v1.xml b/bar/services/river/protocols/river-control-unstable-v1.xml new file mode 100644 index 0000000..aa5fc4d --- /dev/null +++ b/bar/services/river/protocols/river-control-unstable-v1.xml @@ -0,0 +1,85 @@ + + + + Copyright 2020 The River Developers + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + + + This interface allows clients to run compositor commands and receive a + success/failure response with output or a failure message respectively. + + Each command is built up in a series of add_argument requests and + executed with a run_command request. The first argument is the command + to be run. + + A complete list of commands should be made available in the man page of + the compositor. + + + + + This request indicates that the client will not use the + river_control object any more. Objects that have been created + through this instance are not affected. + + + + + + Arguments are stored by the server in the order they were sent until + the run_command request is made. + + + + + + + Execute the command built up using the add_argument request for the + given seat. + + + + + + + + + This object is created by the run_command request. Exactly one of the + success or failure events will be sent. This object will be destroyed + by the compositor after one of the events is sent. + + + + + Sent when the command has been successfully received and executed by + the compositor. Some commands may produce output, in which case the + output argument will be a non-empty string. + + + + + + + Sent when the command could not be carried out. This could be due to + sending a non-existent command, no command, not enough arguments, too + many arguments, invalid arguments, etc. + + + + + diff --git a/bar/services/river/protocols/river-status-unstable-v1.xml b/bar/services/river/protocols/river-status-unstable-v1.xml new file mode 100644 index 0000000..e9629dd --- /dev/null +++ b/bar/services/river/protocols/river-status-unstable-v1.xml @@ -0,0 +1,148 @@ + + + + Copyright 2020 The River Developers + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + + + A global factory for objects that receive status information specific + to river. It could be used to implement, for example, a status bar. + + + + + This request indicates that the client will not use the + river_status_manager object any more. Objects that have been created + through this instance are not affected. + + + + + + This creates a new river_output_status object for the given wl_output. + + + + + + + + This creates a new river_seat_status object for the given wl_seat. + + + + + + + + + This interface allows clients to receive information about the current + windowing state of an output. + + + + + This request indicates that the client will not use the + river_output_status object any more. + + + + + + Sent once binding the interface and again whenever the tag focus of + the output changes. + + + + + + + Sent once on binding the interface and again whenever the tag state + of the output changes. + + + + + + + Sent once on binding the interface and again whenever the set of + tags with at least one urgent view changes. + + + + + + + Sent once on binding the interface should a layout name exist and again + whenever the name changes. + + + + + + + Sent when the current layout name has been removed without a new one + being set, for example when the active layout generator disconnects. + + + + + + + This interface allows clients to receive information about the current + focus of a seat. Note that (un)focused_output events will only be sent + if the client has bound the relevant wl_output globals. + + + + + This request indicates that the client will not use the + river_seat_status object any more. + + + + + + Sent on binding the interface and again whenever an output gains focus. + + + + + + + Sent whenever an output loses focus. + + + + + + + Sent once on binding the interface and again whenever the focused + view or a property thereof changes. The title may be an empty string + if no view is focused or the focused view did not set a title. + + + + + + + Sent once on binding the interface and again whenever a new mode + is entered (e.g. with riverctl enter-mode foobar). + + + + + diff --git a/bar/services/river/service.py b/bar/services/river/service.py new file mode 100644 index 0000000..7d0aa19 --- /dev/null +++ b/bar/services/river/service.py @@ -0,0 +1,352 @@ +import os +import time +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Set + +from fabric.core.service import Property, Service, Signal +from fabric.utils.helpers import idle_add +from gi.repository import GLib +from loguru import logger + +# Import pywayland components - ensure these imports are correct +from pywayland.client import Display +from pywayland.protocol.wayland import WlOutput, WlSeat + +from .protocols.generated.river_control_unstable_v1 import ZriverControlV1 +from .protocols.generated.river_status_unstable_v1 import ZriverStatusManagerV1 + + +@dataclass +class OutputInfo: + """Information about a River output""" + + name: int + output: WlOutput + status: Any = None # ZriverOutputStatusV1 + tags_view: List[int] = field(default_factory=list) + tags_focused: List[int] = field(default_factory=list) + tags_urgent: List[int] = field(default_factory=list) + + +@dataclass(frozen=True) +class RiverEvent: + """Event data from River compositor""" + + name: str + data: List[Any] + output_id: Optional[int] = None + + +class River(Service): + """Connection to River Wayland compositor via river-status protocol""" + + @Property(bool, "readable", "is-ready", default_value=False) + def ready(self) -> bool: + return self._ready + + @Property(str, "readable", "active-window", default_value="") + def active_window(self) -> str: + """Get the title of the currently active window""" + return self._active_window_title + + @Signal + def ready_signal(self): + return self.notify("ready") + + @Signal("event", flags="detailed") + def event(self, event: object): ... + + def __init__(self, **kwargs): + """Initialize the River service""" + super().__init__(**kwargs) + self._ready = False + self._active_window_title = "" + self.outputs: Dict[int, OutputInfo] = {} + self._display = None + self.river_status_mgr = None + self.river_control = None + self.seat = None + self.seat_status = None + + # Start the connection in a separate thread + self.river_thread = GLib.Thread.new( + "river-status-service", self._river_connection_task + ) + + def _river_connection_task(self): + """Main thread that connects to River and listens for events""" + try: + logger.info("[RiverService] Starting connection to River") + + logger.debug( + f"[RiverService] XDG_RUNTIME_DIR={os.environ.get('XDG_RUNTIME_DIR', 'Not set')}" + ) + logger.debug( + f"[RiverService] WAYLAND_DISPLAY={os.environ.get('WAYLAND_DISPLAY', 'Not set')}" + ) + + self._display = Display() + self._display.connect() + + # Get the registry + registry = self._display.get_registry() + logger.debug("[RiverService] Registry obtained") + + # Create state object to hold our data + state = { + "display": self._display, + "registry": registry, + "outputs": {}, + "river_status_mgr": None, + "river_control": None, + "seat": None, + "seat_status": None, + } + + def handle_global(registry, name, iface, version): + logger.debug( + f"[RiverService] Global: {iface} (v{version}, name={name})" + ) + if iface == "zriver_status_manager_v1": + state["river_status_mgr"] = registry.bind( + name, ZriverStatusManagerV1, version + ) + logger.info("[RiverService] Found river status manager") + elif iface == "zriver_control_v1": + state["river_control"] = registry.bind( + name, ZriverControlV1, version + ) + logger.info("[RiverService] Found river control interface") + elif iface == "wl_output": + output = registry.bind(name, WlOutput, version) + state["outputs"][name] = OutputInfo(name=name, output=output) + logger.info(f"[RiverService] Found output {name}") + elif iface == "wl_seat": + state["seat"] = registry.bind(name, WlSeat, version) + logger.info("[RiverService] Found seat") + + def handle_global_remove(registry, name): + if name in state["outputs"]: + logger.info(f"[RiverService] Output {name} removed") + del state["outputs"][name] + idle_add( + lambda: self.emit( + "event::output_removed", + RiverEvent("output_removed", [name]), + ) + ) + + # Set up the dispatchers + registry.dispatcher["global"] = handle_global + registry.dispatcher["global_remove"] = handle_global_remove + + # Discover globals + logger.debug("[RiverService] Performing initial roundtrip") + self._display.roundtrip() + + # Check if we found the river status manager + if not state["river_status_mgr"]: + logger.error("[RiverService] River status manager not found") + return + + # Handle the window title updates through seat status + + if not state["river_control"]: + logger.error( + "[RiverService] River control interface not found - falling back to riverctl" + ) + # You could still fall back to the old riverctl method here if needed + + def focused_view_handler(_, title): + logger.debug(f"[RiverService] Focused view title: {title}") + self._active_window_title = title + idle_add(lambda: self._emit_active_window(title)) + + # Get the seat status to track active window + + if state["seat"]: + seat_status = state["river_status_mgr"].get_river_seat_status( + state["seat"] + ) + seat_status.dispatcher["focused_view"] = focused_view_handler + state["seat_status"] = seat_status + logger.info("[RiverService] Set up seat status for window tracking") + + # Create view tags and focused tags handlers + def make_view_tags_handler(output_id): + def handler(_, tags): + decoded = self._decode_bitfields(tags) + state["outputs"][output_id].tags_view = decoded + logger.debug( + f"[RiverService] Output {output_id} view tags: {decoded}" + ) + idle_add(lambda: self._emit_view_tags(output_id, decoded)) + + return handler + + def make_focused_tags_handler(output_id): + def handler(_, tags): + decoded = self._decode_bitfields(tags) + state["outputs"][output_id].tags_focused = decoded + logger.debug( + f"[RiverService] Output {output_id} focused tags: {decoded}" + ) + idle_add(lambda: self._emit_focused_tags(output_id, decoded)) + + return handler + + def make_urgent_tags_handler(output_id): + def handler(_, tags): + decoded = self._decode_bitfields(tags) + state["outputs"][output_id].tags_urgent = decoded + logger.debug( + f"[RiverService] Output {output_id} urgent tags: {decoded}" + ) + idle_add(lambda: self._emit_urgent_tags(output_id, decoded)) + + return handler + + # Bind output status listeners + for name, info in list(state["outputs"].items()): + status = state["river_status_mgr"].get_river_output_status(info.output) + status.dispatcher["view_tags"] = make_view_tags_handler(name) + status.dispatcher["focused_tags"] = make_focused_tags_handler(name) + status.dispatcher["urgent_tags"] = make_urgent_tags_handler(name) + info.status = status + logger.info(f"[RiverService] Set up status for output {name}") + + # Initial data fetch + logger.debug("[RiverService] Performing second roundtrip") + self._display.roundtrip() + + # Update our outputs dictionary + self.outputs.update(state["outputs"]) + self.river_status_mgr = state["river_status_mgr"] + self.river_control = state["river_control"] + self.seat = state["seat"] + self.seat_status = state.get("seat_status") + + # Mark service as ready + idle_add(self._set_ready) + + while True: + self._display.dispatch(block=True) + + except Exception as e: + logger.error(f"[RiverService] Error in River connection: {e}") + import traceback + + logger.error(traceback.format_exc()) + + return True + + def _set_ready(self): + """Set the service as ready (called on main thread via idle_add)""" + self._ready = True + logger.info("[RiverService] Service ready") + self.ready_signal.emit() + return False # Don't repeat + + def _emit_view_tags(self, output_id, tags): + """Emit view_tags events (called on main thread)""" + event = RiverEvent("view_tags", tags, output_id) + self.emit("event::view_tags", event) + self.emit(f"event::view_tags::{output_id}", tags) + return False # Don't repeat + + def _emit_focused_tags(self, output_id, tags): + """Emit focused_tags events (called on main thread)""" + event = RiverEvent("focused_tags", tags, output_id) + self.emit("event::focused_tags", event) + self.emit(f"event::focused_tags::{output_id}", tags) + return False # Don't repeat + + def _emit_active_window(self, title): + """Emit active window title events (called on main thread)""" + event = RiverEvent("active_window", [title]) + self.emit("event::active_window", event) + self.notify("active-window") + return False # Don't repeat + + def _emit_urgent_tags(self, output_id, tags): + """Emit urgent_tags events (called on main thread)""" + event = RiverEvent("urgent_tags", tags, output_id) + self.emit("event::urgent_tags", event) + self.emit(f"event::urgent_tags::{output_id}", tags) + return False # Don't repeat + + @staticmethod + def _decode_bitfields(bitfields) -> List[int]: + """Decode River's tag bitfields into a list of tag indices""" + tags: Set[int] = set() + + # Ensure we have an iterable + if not hasattr(bitfields, "__iter__"): + bitfields = [bitfields] + + for bits in bitfields: + for i in range(32): + if bits & (1 << i): + tags.add(i) + + return sorted(tags) + + def run_command(self, command, *args, callback=None): + """Run a riverctl command""" + if not self.river_control or not self.seat: + logger.warning( + "[RiverService] River control or seat not available, falling back to riverctl" + ) + return self._run_command_fallback(command, *args) + + self.river_control.add_argument(command) + for arg in args: + self.river_control.add_argument(str(arg)) + + # Execute the command + command_callback = self.river_control.run_command(self.seat) + + # Set up callback handlers + result = {"stdout": None, "stderr": None, "success": None} + + def handle_success(_, output): + logger.debug(f"[RiverService] Command success: {output}") + result["stdout"] = output + result["success"] = True + if callback: + idle_add(lambda: callback(True, output, None)) + + def handle_failure(_, failure_message): + logger.debug(f"[RiverService] Command failure: {failure_message}") + result["stderr"] = failure_message + result["success"] = False + if callback: + idle_add(lambda: callback(False, None, failure_message)) + + command_callback.dispatcher["success"] = handle_success + command_callback.dispatcher["failure"] = handle_failure + + if hasattr(self, "_display"): + self._display.flush() + + return True + + def _run_command_fallback(self, command, *args): + """Fallback to riverctl""" + import subprocess + + cmd = ["riverctl", command] + [str(arg) for arg in args] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + logger.info(f"[RiverService] Ran command: {' '.join(cmd)}") + return result.stdout.strip() + except subprocess.CalledProcessError as e: + logger.error( + f"[RiverService] Command failed: {' '.join(cmd)}, error: {e.stderr}" + ) + return None + + def toggle_focused_tag(self, tag, callback=None): + """Toggle a tag in the focused tags""" + tag_mask = 1 << int(tag) + self.run_command("set-focused-tags", str(tag_mask), callback=callback) diff --git a/bar/services/river/widgets.py b/bar/services/river/widgets.py new file mode 100644 index 0000000..c31b384 --- /dev/null +++ b/bar/services/river/widgets.py @@ -0,0 +1,266 @@ +from fabric.core.service import Property +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.eventbox import EventBox +from fabric.widgets.label import Label +from gi.repository import Gdk +from loguru import logger + +from .service import River + +connection: River | None = None + + +def get_river_connection() -> River: + global connection + if not connection: + connection = River() + return connection + + +class RiverWorkspaceButton(Button): + @Property(int, "readable") + def id(self) -> int: + return self._id + + @Property(bool, "read-write", default_value=False) + def active(self) -> bool: + return self._active + + @active.setter + def active(self, value: bool): + self._active = value + (self.remove_style_class if not value else self.add_style_class)("active") + + @Property(bool, "read-write", default_value=False) + def empty(self) -> bool: + return self._empty + + @empty.setter + def empty(self, value: bool): + self._empty = value + (self.remove_style_class if not value else self.add_style_class)("empty") + + @Property(bool, "read-write", default_value=False) + def urgent(self) -> bool: + return self._urgent + + @urgent.setter + def urgent(self, value: bool): + self._urgent = value + self._update_style() + + def __init__(self, id: int, label: str = None, **kwargs): + super().__init__(label or str(id), **kwargs) + self._id = id + self._active = False + self._empty = True + self._urgent = False + + def _update_style(self): + """Update button styles based on states""" + # Remove all state-related styles first + self.remove_style_class("active") + self.remove_style_class("empty") + self.remove_style_class("urgent") + + # Then apply current states + if self._active: + self.add_style_class("active") + if self._empty: + self.add_style_class("empty") + if self._urgent: + self.add_style_class("urgent") + + +class RiverWorkspaces(EventBox): + def __init__(self, output_id, river_service=None, max_tags=9, **kwargs): + super().__init__(events="scroll") + self._box = Box(**kwargs) + self.children = self._box + + if river_service: + self.river = river_service + + # Store output_id as received + self.output_id = output_id + + self.max_tags = max_tags + # Create buttons for tags 0 to max_tags-1 (to match River's 0-based tag indexing) + self._buttons = {i: RiverWorkspaceButton(i) for i in range(max_tags)} + + for btn in self._buttons.values(): + btn.connect("clicked", self.on_workspace_click) + self._box.add(btn) + + # Connect to service events + self.river.connect("event::focused_tags", self.on_focus_change_general) + self.river.connect("event::view_tags", self.on_view_change_general) + self.river.connect("event::urgent_tags", self.on_urgent_change_general) + self.river.connect("event::output_removed", self.on_output_removed) + + # Initial setup when service is ready + if self.river.ready: + self.on_ready(None) + else: + self.river.connect("event::ready", self.on_ready) + + self.connect("scroll-event", self.on_scroll) + + def on_ready(self, _): + """Initialize widget state when service is ready""" + + if self.output_id is None and self.river.outputs: + self.output_id = next(iter(self.river.outputs.keys())) + logger.info(f"[RiverWorkspaces] Selected output {self.output_id}") + + if self.output_id is not None and self.output_id in self.river.outputs: + output_info = self.river.outputs[self.output_id] + + focused_tags = output_info.tags_focused + view_tags = output_info.tags_view + urgent_tags = output_info.tags_urgent + + for i, btn in self._buttons.items(): + btn.active = i in focused_tags + btn.empty = i not in view_tags + btn.urgent = i in urgent_tags + + def on_focus_change(self, _, tags): + """Handle focused tags change for our specific output""" + logger.info( + f"[RiverWorkspaces] Focus change on output {self.output_id}: {tags}" + ) + for i, btn in self._buttons.items(): + btn.active = i in tags + + def on_view_change(self, _, tags): + """Handle view tags change for our specific output""" + logger.info(f"[RiverWorkspaces] View change on output {self.output_id}: {tags}") + for i, btn in self._buttons.items(): + btn.empty = i not in tags + + def on_focus_change_general(self, _, event): + """Handle general focused tags event""" + # Only handle event if it's for our output + if event.output_id == self.output_id: + logger.info( + f"[RiverWorkspaces] General focus change for output {self.output_id}" + ) + self.on_focus_change(_, event.data) + + def on_view_change_general(self, _, event): + """Handle general view tags event""" + # Only handle event if it's for our output + if event.output_id == self.output_id: + logger.info( + f"[RiverWorkspaces] General view change for output {self.output_id}" + ) + self.on_view_change(_, event.data) + + def on_urgent_change(self, _, tags): + """Handle urgent tags change for our specific output""" + logger.info( + f"[RiverWorkspaces] Urgent change on output {self.output_id}: {tags}" + ) + for i, btn in self._buttons.items(): + btn.urgent = i in tags + + def on_urgent_change_general(self, _, event): + """Handle general urgent tags event""" + # Only handle event if it's for our output + if event.output_id == self.output_id: + logger.info( + f"[RiverWorkspaces] General urgent change for output {self.output_id}" + ) + self.on_urgent_change(_, event.data) + + def on_output_removed(self, _, event): + """Handle output removal""" + removed_id = event.data[0] + + if removed_id == self.output_id: + logger.info(f"[RiverWorkspaces] Our output {self.output_id} was removed") + + # Try to find another output + if self.river.outputs: + self.output_id = next(iter(self.river.outputs.keys())) + logger.info(f"[RiverWorkspaces] Switching to output {self.output_id}") + + # Update state for new output + if self.output_id in self.river.outputs: + output_info = self.river.outputs[self.output_id] + # Access fields directly on the OutputInfo dataclass + focused_tags = output_info.tags_focused + view_tags = output_info.tags_view + + for i, btn in self._buttons.items(): + btn.active = i in focused_tags + btn.empty = i not in view_tags + + def on_workspace_click(self, btn): + """Handle workspace button click""" + logger.info(f"[RiverWorkspaces] Clicked on workspace {btn.id}") + self.river.toggle_focused_tag(btn.id) + + def on_scroll(self, _, event): + """Handle scroll events""" + direction = event.direction + if direction == Gdk.ScrollDirection.DOWN: + logger.info("[RiverWorkspaces] Scroll down - focusing next view") + self.river.run_command("focus-view", "next") + elif direction == Gdk.ScrollDirection.UP: + logger.info("[RiverWorkspaces] Scroll up - focusing previous view") + self.river.run_command("focus-view", "previous") + + +class RiverActiveWindow(Label): + """Widget to display the currently active window's title""" + + def __init__(self, max_length=None, ellipsize="end", river_service=None, **kwargs): + super().__init__(**kwargs) + + if river_service: + self.river = river_service + + self.max_length = max_length + self.ellipsize = ellipsize + + # Set initial state + if self.river.ready: + self.on_ready(None) + else: + self.river.connect("event::ready", self.on_ready) + + # Connect to active window changes + self.river.connect("event::active_window", self.on_active_window_changed) + + def on_ready(self, _): + """Initialize widget when service is ready""" + logger.info("[RiverActiveWindow] Connected to service") + self.update_title(self.river.active_window) + + def on_active_window_changed(self, _, event): + """Update widget when active window changes""" + title = event.data[0] if event.data else "" + logger.debug(f"[RiverActiveWindow] Window changed to: {title}") + self.update_title(title) + + def update_title(self, title): + """Update the label with the window title""" + if not title: + self.label = "" + self.set_label(self.label) + return + + if self.max_length and len(title) > self.max_length: + if self.ellipsize == "end": + title = title[: self.max_length] + "..." + elif self.ellipsize == "middle": + half = (self.max_length - 3) // 2 + title = title[:half] + "..." + title[-half:] + elif self.ellipsize == "start": + title = "..." + title[-self.max_length :] + + self.label = title + self.set_label(self.label) diff --git a/bar/services/wlr/event_loop.py b/bar/services/wlr/event_loop.py new file mode 100644 index 0000000..71e9be0 --- /dev/null +++ b/bar/services/wlr/event_loop.py @@ -0,0 +1,21 @@ +from fabric.core.service import Service, Property +from pywayland.client import Display +from gi.repository import GLib + + +class WaylandEventLoopService(Service): + @Property(object, "readable", "display") + def display_property(self): + return self._display + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._display = Display() + self._display.connect() + + self.thread = GLib.Thread.new("wayland-loop", self._loop) + + def _loop(self): + while True: + self._display.dispatch(block=True) + print("DISPATCHING...") diff --git a/bar/services/wlr/protocol/windows.py b/bar/services/wlr/protocol/windows.py new file mode 100644 index 0000000..3d4578e --- /dev/null +++ b/bar/services/wlr/protocol/windows.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 + +import sys +from typing import Dict, List, Optional + +import pywayland +from pywayland.client import Display +from pywayland.protocol.wayland import WlOutput, WlSeat + +# Import the protocol interfaces from your files +from wlr_foreign_toplevel_management_unstable_v1.zwlr_foreign_toplevel_manager_v1 import ( + ZwlrForeignToplevelManagerV1, +) +from wlr_foreign_toplevel_management_unstable_v1.zwlr_foreign_toplevel_handle_v1 import ( + ZwlrForeignToplevelHandleV1, +) + + +class Window: + """Represents a toplevel window in the compositor.""" + + def __init__(self, handle: ZwlrForeignToplevelHandleV1): + self.handle = handle + self.title: str = "Unknown" + self.app_id: str = "Unknown" + self.states: List[str] = [] + self.outputs: List[WlOutput] = [] + self.parent: Optional["Window"] = None + self.closed = False + + def __str__(self) -> str: + state_str = ( + ", ".join([ZwlrForeignToplevelHandleV1.state(s).name for s in self.states]) + if self.states + else "normal" + ) + return ( + f"Window(title='{self.title}', app_id='{self.app_id}', state={state_str})" + ) + + +class WaylandWindowManager: + """Manages Wayland windows using the foreign toplevel protocol.""" + + def __init__(self): + self.display = Display() + self.windows: Dict[ZwlrForeignToplevelHandleV1, Window] = {} + self.manager = None + self.running = False + + def connect(self) -> bool: + """Connect to the Wayland display and bind to the toplevel manager.""" + try: + self.display.connect() + print("Connected to Wayland display") + + # Get the registry to find the foreign toplevel manager + registry = self.display.get_registry() + registry.dispatcher["global"] = self._registry_global_handler + + # Roundtrip to process registry events + self.display.roundtrip() + + if not self.manager: + print( + "Foreign toplevel manager not found. Is wlr-foreign-toplevel-management protocol supported?" + ) + return False + + return True + + except Exception as e: + print(f"Failed to connect: {e}") + return False + + def _registry_global_handler(self, registry, id, interface, version): + """Handle registry global objects.""" + if interface == ZwlrForeignToplevelManagerV1.name: + print(f"Found foreign toplevel manager (id={id}, version={version})") + self.manager = registry.bind( + id, ZwlrForeignToplevelManagerV1, min(version, 3) + ) + self.manager.dispatcher["toplevel"] = self._handle_toplevel + self.manager.dispatcher["finished"] = self._handle_manager_finished + + def _handle_toplevel(self, manager, toplevel): + """Handle a new toplevel window.""" + window = Window(toplevel) + self.windows[toplevel] = window + print(window) + + # Setup event dispatchers for the toplevel + toplevel.dispatcher["title"] = self._handle_title + toplevel.dispatcher["app_id"] = self._handle_app_id + toplevel.dispatcher["state"] = self._handle_state + toplevel.dispatcher["done"] = self._handle_done + toplevel.dispatcher["closed"] = self._handle_closed + toplevel.dispatcher["output_enter"] = self._handle_output_enter + toplevel.dispatcher["output_leave"] = self._handle_output_leave + + def _handle_title(self, toplevel, title): + """Handle toplevel title changes.""" + window = self.windows.get(toplevel) + if window: + window.title = title + + def _handle_app_id(self, toplevel, app_id): + """Handle toplevel app_id changes.""" + window = self.windows.get(toplevel) + if window: + window.app_id = app_id + + def _handle_state(self, toplevel, states): + """Handle toplevel state changes.""" + window = self.windows.get(toplevel) + if window: + window.states = states + + def _handle_done(self, toplevel): + """Handle toplevel done event.""" + window = self.windows.get(toplevel) + if window and not window.closed: + print(f"Window updated: {window}") + + def _handle_closed(self, toplevel): + """Handle toplevel closed event.""" + window = self.windows.get(toplevel) + if window: + window.closed = True + print(f"Window closed: {window}") + # Clean up the toplevel object + toplevel.destroy() + # Remove from our dictionary + del self.windows[toplevel] + + def _handle_output_enter(self, toplevel, output): + """Handle toplevel entering an output.""" + window = self.windows.get(toplevel) + if window and output not in window.outputs: + window.outputs.append(output) + + def _handle_output_leave(self, toplevel, output): + """Handle toplevel leaving an output.""" + window = self.windows.get(toplevel) + if window and output in window.outputs: + window.outputs.remove(output) + + def _handle_parent(self, toplevel, parent): + """Handle toplevel parent changes.""" + window = self.windows.get(toplevel) + if window: + if parent is None: + window.parent = None + else: + parent_window = self.windows.get(parent) + if parent_window: + window.parent = parent_window + + def _handle_manager_finished(self, manager): + """Handle manager finished event.""" + print("Foreign toplevel manager finished") + self.running = False + + def get_windows(self) -> List[Window]: + """Get all currently active windows.""" + # Filter out closed windows + active_windows = [ + window for window in self.windows.values() if not window.closed + ] + return active_windows + + def run(self): + """Run the event loop to receive window updates.""" + self.running = True + print("Listening for window events (press Ctrl+C to exit)...") + + try: + while self.running: + self.display.dispatch(block=True) + except KeyboardInterrupt: + print("\nExiting...") + finally: + self.cleanup() + + def cleanup(self): + """Clean up resources.""" + print("cleanup") + if self.manager: + self.manager.stop() + + # Destroy all toplevel handles + for toplevel, window in list(self.windows.items()): + if not window.closed: + toplevel.destroy() + + # Disconnect from display + if self.display: + self.display.disconnect() + + self.running = False + + +def main(): + """Main entry point.""" + manager = WaylandWindowManager() + + if not manager.connect(): + return 1 + + # # Run for a short time to collect initial windows + for _ in range(1): + manager.display.dispatch(block=True) + + # Print all windows + windows = manager.get_windows() + print("\nActive windows:") + if windows: + for i, window in enumerate(windows, 1): + print(f"{i}. {window}") + else: + print("No windows found") + + # # Option to keep monitoring window events + # if len(sys.argv) > 1 and sys.argv[1] == "--monitor": + # manager.run() + # else: + manager.cleanup() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bar/services/wlr/protocol/wlr-foreign-toplevel-management-unstable-v1.xml b/bar/services/wlr/protocol/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 0000000..44505bb --- /dev/null +++ b/bar/services/wlr/protocol/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,270 @@ + + + + Copyright © 2018 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + + + + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + + + + diff --git a/bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/__init__.py b/bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/__init__.py new file mode 100644 index 0000000..7db60d7 --- /dev/null +++ b/bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/__init__.py @@ -0,0 +1,27 @@ +# This file has been autogenerated by the pywayland scanner + +# Copyright © 2018 Ilia Bozhinov +# +# Permission to use, copy, modify, distribute, and sell this +# software and its documentation for any purpose is hereby granted +# without fee, provided that the above copyright notice appear in +# all copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# the copyright holders not be used in advertising or publicity +# pertaining to distribution of the software without specific, +# written prior permission. The copyright holders make no +# representations about the suitability of this software for any +# purpose. It is provided "as is" without express or implied +# warranty. +# +# THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS +# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +# SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +# AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +# THIS SOFTWARE. + +from .zwlr_foreign_toplevel_handle_v1 import ZwlrForeignToplevelHandleV1 # noqa: F401 +from .zwlr_foreign_toplevel_manager_v1 import ZwlrForeignToplevelManagerV1 # noqa: F401 diff --git a/bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/zwlr_foreign_toplevel_handle_v1.py b/bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/zwlr_foreign_toplevel_handle_v1.py new file mode 100644 index 0000000..81bf01b --- /dev/null +++ b/bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/zwlr_foreign_toplevel_handle_v1.py @@ -0,0 +1,352 @@ +# This file has been autogenerated by the pywayland scanner + +# Copyright © 2018 Ilia Bozhinov +# +# Permission to use, copy, modify, distribute, and sell this +# software and its documentation for any purpose is hereby granted +# without fee, provided that the above copyright notice appear in +# all copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# the copyright holders not be used in advertising or publicity +# pertaining to distribution of the software without specific, +# written prior permission. The copyright holders make no +# representations about the suitability of this software for any +# purpose. It is provided "as is" without express or implied +# warranty. +# +# THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS +# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +# SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +# AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +# THIS SOFTWARE. + +from __future__ import annotations + +import enum + +from pywayland.protocol_core import ( + Argument, + ArgumentType, + Global, + Interface, + Proxy, + Resource, +) + +from pywayland.protocol.wayland import WlOutput +from pywayland.protocol.wayland import WlSeat +from pywayland.protocol.wayland import WlSurface + + +class ZwlrForeignToplevelHandleV1(Interface): + """An opened toplevel + + A :class:`ZwlrForeignToplevelHandleV1` object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + """ + + name = "zwlr_foreign_toplevel_handle_v1" + version = 3 + + class state(enum.IntEnum): + maximized = 0 + minimized = 1 + activated = 2 + fullscreen = 3 + + class error(enum.IntEnum): + invalid_rectangle = 0 + + +class ZwlrForeignToplevelHandleV1Proxy(Proxy[ZwlrForeignToplevelHandleV1]): + interface = ZwlrForeignToplevelHandleV1 + + @ZwlrForeignToplevelHandleV1.request() + def set_maximized(self) -> None: + """Requests that the toplevel be maximized + + Requests that the toplevel be maximized. If the maximized state + actually changes, this will be indicated by the state event. + """ + self._marshal(0) + + @ZwlrForeignToplevelHandleV1.request() + def unset_maximized(self) -> None: + """Requests that the toplevel be unmaximized + + Requests that the toplevel be unmaximized. If the maximized state + actually changes, this will be indicated by the state event. + """ + self._marshal(1) + + @ZwlrForeignToplevelHandleV1.request() + def set_minimized(self) -> None: + """Requests that the toplevel be minimized + + Requests that the toplevel be minimized. If the minimized state + actually changes, this will be indicated by the state event. + """ + self._marshal(2) + + @ZwlrForeignToplevelHandleV1.request() + def unset_minimized(self) -> None: + """Requests that the toplevel be unminimized + + Requests that the toplevel be unminimized. If the minimized state + actually changes, this will be indicated by the state event. + """ + self._marshal(3) + + @ZwlrForeignToplevelHandleV1.request( + Argument(ArgumentType.Object, interface=WlSeat), + ) + def activate(self, seat: WlSeat) -> None: + """Activate the toplevel + + Request that this toplevel be activated on the given seat. There is no + guarantee the toplevel will be actually activated. + + :param seat: + :type seat: + :class:`~pywayland.protocol.wayland.WlSeat` + """ + self._marshal(4, seat) + + @ZwlrForeignToplevelHandleV1.request() + def close(self) -> None: + """Request that the toplevel be closed + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives no + guarantees the toplevel will actually be destroyed. If and when this + happens, the :func:`ZwlrForeignToplevelHandleV1.closed()` event will be + emitted. + """ + self._marshal(5) + + @ZwlrForeignToplevelHandleV1.request( + Argument(ArgumentType.Object, interface=WlSurface), + Argument(ArgumentType.Int), + Argument(ArgumentType.Int), + Argument(ArgumentType.Int), + Argument(ArgumentType.Int), + ) + def set_rectangle( + self, surface: WlSurface, x: int, y: int, width: int, height: int + ) -> None: + """The rectangle which represents the toplevel + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given + toplevel. It can be used by the compositor as a hint for some + operations, e.g minimizing. The client is however not required to set + this, in which case the compositor is free to decide some default + value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. Setting + width=height=0 removes the already-set rectangle. + + :param surface: + :type surface: + :class:`~pywayland.protocol.wayland.WlSurface` + :param x: + :type x: + `ArgumentType.Int` + :param y: + :type y: + `ArgumentType.Int` + :param width: + :type width: + `ArgumentType.Int` + :param height: + :type height: + `ArgumentType.Int` + """ + self._marshal(6, surface, x, y, width, height) + + @ZwlrForeignToplevelHandleV1.request() + def destroy(self) -> None: + """Destroy the :class:`ZwlrForeignToplevelHandleV1` object + + Destroys the :class:`ZwlrForeignToplevelHandleV1` object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + """ + self._marshal(7) + self._destroy() + + @ZwlrForeignToplevelHandleV1.request( + Argument(ArgumentType.Object, interface=WlOutput, nullable=True), + version=2, + ) + def set_fullscreen(self, output: WlOutput | None) -> None: + """Request that the toplevel be fullscreened + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + :param output: + :type output: + :class:`~pywayland.protocol.wayland.WlOutput` or `None` + """ + self._marshal(8, output) + + @ZwlrForeignToplevelHandleV1.request(version=2) + def unset_fullscreen(self) -> None: + """Request that the toplevel be unfullscreened + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + """ + self._marshal(9) + + +class ZwlrForeignToplevelHandleV1Resource(Resource): + interface = ZwlrForeignToplevelHandleV1 + + @ZwlrForeignToplevelHandleV1.event( + Argument(ArgumentType.String), + ) + def title(self, title: str) -> None: + """Title change + + This event is emitted whenever the title of the toplevel changes. + + :param title: + :type title: + `ArgumentType.String` + """ + self._post_event(0, title) + + @ZwlrForeignToplevelHandleV1.event( + Argument(ArgumentType.String), + ) + def app_id(self, app_id: str) -> None: + """App-id change + + This event is emitted whenever the app-id of the toplevel changes. + + :param app_id: + :type app_id: + `ArgumentType.String` + """ + self._post_event(1, app_id) + + @ZwlrForeignToplevelHandleV1.event( + Argument(ArgumentType.Object, interface=WlOutput), + ) + def output_enter(self, output: WlOutput) -> None: + """Toplevel entered an output + + This event is emitted whenever the toplevel becomes visible on the + given output. A toplevel may be visible on multiple outputs. + + :param output: + :type output: + :class:`~pywayland.protocol.wayland.WlOutput` + """ + self._post_event(2, output) + + @ZwlrForeignToplevelHandleV1.event( + Argument(ArgumentType.Object, interface=WlOutput), + ) + def output_leave(self, output: WlOutput) -> None: + """Toplevel left an output + + This event is emitted whenever the toplevel stops being visible on the + given output. It is guaranteed that an entered-output event with the + same output has been emitted before this event. + + :param output: + :type output: + :class:`~pywayland.protocol.wayland.WlOutput` + """ + self._post_event(3, output) + + @ZwlrForeignToplevelHandleV1.event( + Argument(ArgumentType.Array), + ) + def state(self, state: list) -> None: + """The toplevel state changed + + This event is emitted immediately after the + zlw_foreign_toplevel_handle_v1 is created and each time the toplevel + state changes, either because of a compositor action or because of a + request in this protocol. + + :param state: + :type state: + `ArgumentType.Array` + """ + self._post_event(4, state) + + @ZwlrForeignToplevelHandleV1.event() + def done(self) -> None: + """All information about the toplevel has been sent + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the :class:`ZwlrForeignToplevelHandleV1` + properties to be seen as atomic, even if they happen via multiple + events. + """ + self._post_event(5) + + @ZwlrForeignToplevelHandleV1.event() + def closed(self) -> None: + """This toplevel has been destroyed + + This event means the toplevel has been destroyed. It is guaranteed + there won't be any more events for this + :class:`ZwlrForeignToplevelHandleV1`. The toplevel itself becomes inert + so any requests will be ignored except the destroy request. + """ + self._post_event(6) + + @ZwlrForeignToplevelHandleV1.event( + Argument( + ArgumentType.Object, interface=ZwlrForeignToplevelHandleV1, nullable=True + ), + version=3, + ) + def parent(self, parent: ZwlrForeignToplevelHandleV1 | None) -> None: + """Parent change + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + :param parent: + :type parent: + :class:`ZwlrForeignToplevelHandleV1` or `None` + """ + self._post_event(7, parent) + + +class ZwlrForeignToplevelHandleV1Global(Global): + interface = ZwlrForeignToplevelHandleV1 + + +ZwlrForeignToplevelHandleV1._gen_c() +ZwlrForeignToplevelHandleV1.proxy_class = ZwlrForeignToplevelHandleV1Proxy +ZwlrForeignToplevelHandleV1.resource_class = ZwlrForeignToplevelHandleV1Resource +ZwlrForeignToplevelHandleV1.global_class = ZwlrForeignToplevelHandleV1Global diff --git a/bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/zwlr_foreign_toplevel_manager_v1.py b/bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/zwlr_foreign_toplevel_manager_v1.py new file mode 100644 index 0000000..83e153f --- /dev/null +++ b/bar/services/wlr/protocol/wlr_foreign_toplevel_management_unstable_v1/zwlr_foreign_toplevel_manager_v1.py @@ -0,0 +1,112 @@ +# This file has been autogenerated by the pywayland scanner + +# Copyright © 2018 Ilia Bozhinov +# +# Permission to use, copy, modify, distribute, and sell this +# software and its documentation for any purpose is hereby granted +# without fee, provided that the above copyright notice appear in +# all copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# the copyright holders not be used in advertising or publicity +# pertaining to distribution of the software without specific, +# written prior permission. The copyright holders make no +# representations about the suitability of this software for any +# purpose. It is provided "as is" without express or implied +# warranty. +# +# THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS +# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +# SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +# AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +# THIS SOFTWARE. + +from __future__ import annotations + +from pywayland.protocol_core import ( + Argument, + ArgumentType, + Global, + Interface, + Proxy, + Resource, +) + +from .zwlr_foreign_toplevel_handle_v1 import ZwlrForeignToplevelHandleV1 + + +class ZwlrForeignToplevelManagerV1(Interface): + """List and control opened apps + + The purpose of this protocol is to enable the creation of taskbars and + docks by providing them with a list of opened applications and letting them + request certain actions on them, like maximizing, etc. + + After a client binds the :class:`ZwlrForeignToplevelManagerV1`, each opened + toplevel window will be sent via the toplevel event + """ + + name = "zwlr_foreign_toplevel_manager_v1" + version = 3 + + +class ZwlrForeignToplevelManagerV1Proxy(Proxy[ZwlrForeignToplevelManagerV1]): + interface = ZwlrForeignToplevelManagerV1 + + @ZwlrForeignToplevelManagerV1.request() + def stop(self) -> None: + """Stop sending events + + Indicates the client no longer wishes to receive events for new + toplevels. However the compositor may emit further toplevel_created + events, until the finished event is emitted. + + The client must not send any more requests after this one. + """ + self._marshal(0) + + +class ZwlrForeignToplevelManagerV1Resource(Resource): + interface = ZwlrForeignToplevelManagerV1 + + @ZwlrForeignToplevelManagerV1.event( + Argument(ArgumentType.NewId, interface=ZwlrForeignToplevelHandleV1), + ) + def toplevel(self, toplevel: ZwlrForeignToplevelHandleV1) -> None: + """A toplevel has been created + + This event is emitted whenever a new toplevel window is created. It is + emitted for all toplevels, regardless of the app that has created them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + :class:`~pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1.ZwlrForeignToplevelHandleV1`. + + :param toplevel: + :type toplevel: + :class:`~pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1.ZwlrForeignToplevelHandleV1` + """ + self._post_event(0, toplevel) + + @ZwlrForeignToplevelManagerV1.event() + def finished(self) -> None: + """The compositor has finished with the toplevel manager + + This event indicates that the compositor is done sending events to the + :class:`ZwlrForeignToplevelManagerV1`. The server will destroy the + object immediately after sending this request, so it will become + invalid and the client should free any resources associated with it. + """ + self._post_event(1) + + +class ZwlrForeignToplevelManagerV1Global(Global): + interface = ZwlrForeignToplevelManagerV1 + + +ZwlrForeignToplevelManagerV1._gen_c() +ZwlrForeignToplevelManagerV1.proxy_class = ZwlrForeignToplevelManagerV1Proxy +ZwlrForeignToplevelManagerV1.resource_class = ZwlrForeignToplevelManagerV1Resource +ZwlrForeignToplevelManagerV1.global_class = ZwlrForeignToplevelManagerV1Global diff --git a/bar/services/wlr/service.py b/bar/services/wlr/service.py new file mode 100644 index 0000000..bce5a11 --- /dev/null +++ b/bar/services/wlr/service.py @@ -0,0 +1,238 @@ +import time +from gi.repository import GLib +from typing import Dict, List, Optional + +from pywayland.client import Display +from pywayland.protocol.wayland import WlOutput, WlSeat + +from fabric.core.service import Property, Service, Signal +from fabric.utils.helpers import idle_add + +from bar.services.wlr.protocol.wlr_foreign_toplevel_management_unstable_v1.zwlr_foreign_toplevel_manager_v1 import ( + ZwlrForeignToplevelManagerV1, +) +from bar.services.wlr.protocol.wlr_foreign_toplevel_management_unstable_v1.zwlr_foreign_toplevel_handle_v1 import ( + ZwlrForeignToplevelHandleV1, +) + + +class Window: + """Represents a toplevel window in the compositor.""" + + def __init__(self, handle: ZwlrForeignToplevelHandleV1): + self.handle = handle + self.title: str = "Unknown" + self.app_id: str = "Unknown" + self.states: List[str] = [] + self.outputs: List[WlOutput] = [] + self.parent: Optional["Window"] = None + self.closed = False + + def __str__(self) -> str: + state_str = ( + ", ".join([ZwlrForeignToplevelHandleV1.state(s).name for s in self.states]) + if self.states + else "normal" + ) + return ( + f"Window(title='{self.title}', app_id='{self.app_id}', state={state_str})" + ) + + +class WaylandWindowTracker(Service): + """Track Wayland windows in the background and provide access on demand.""" + + @Property(bool, "readable", "is-ready", default_value=False) + def ready(self) -> bool: + return self._ready + + @Signal + def ready_signal(self): + return self.notify("ready") + + @Property(list[Window], "readable", "windows") + def windows(self) -> list[Window]: + return [window for window in self._window_dict.values() if not window.closed] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.display = None + self._window_dict: Dict[ZwlrForeignToplevelHandleV1, Window] = {} + self._windows = [] + self.manager = None + self.seat: Optional[WlSeat] = None + + self.thread = GLib.Thread.new( + "wayland-window-service", self._run_display_thread + ) + + def _run_display_thread(self): + """Run the Wayland event loop in a background thread.""" + try: + self.display = Display() + self.display.connect() + + # Get the registry to find the foreign toplevel manager + registry = self.display.get_registry() + registry.dispatcher["global"] = self._registry_global_handler + + # Process registry events + self.display.roundtrip() + + if not self.manager: + print("Foreign toplevel manager not found") + return + + # Process more events to get initial windows + for _ in range(5): + self.display.roundtrip() + + idle_add(self._set_ready) + + while True: + self.display.dispatch(block=True) + + except Exception as e: + print(f"Display thread error: {e}") + finally: + self.cleanup() + + def _registry_global_handler(self, registry, id, interface, version): + """Handle registry global objects.""" + if interface == WlSeat.name: + self.seat = registry.bind(id, WlSeat, version) + print(f"Found seat (id={id}, version={version})") + elif interface == ZwlrForeignToplevelManagerV1.name: + self.manager = registry.bind( + id, ZwlrForeignToplevelManagerV1, min(version, 3) + ) + self.manager.dispatcher["toplevel"] = self._handle_toplevel + self.manager.dispatcher["finished"] = self._handle_manager_finished + + def _handle_toplevel(self, manager, toplevel): + """Handle a new toplevel window.""" + print("TOPLEVEL IS TRIGGERD") + window = Window(toplevel) + + self._window_dict[toplevel] = window + + # Setup event dispatchers for the toplevel + toplevel.dispatcher["title"] = self._handle_title + toplevel.dispatcher["app_id"] = self._handle_app_id + toplevel.dispatcher["state"] = self._handle_state + toplevel.dispatcher["done"] = self._handle_done + toplevel.dispatcher["closed"] = self._handle_closed + toplevel.dispatcher["output_enter"] = self._handle_output_enter + toplevel.dispatcher["output_leave"] = self._handle_output_leave + + def _handle_title(self, toplevel, title): + """Handle toplevel title changes.""" + window = self._window_dict.get(toplevel) + if window: + print("there is a window, putting title") + window.title = title + + def _handle_app_id(self, toplevel, app_id): + """Handle toplevel app_id changes.""" + window = self._window_dict.get(toplevel) + if window: + window.app_id = app_id + + def _handle_state(self, toplevel, states): + """Handle toplevel state changes.""" + window = self._window_dict.get(toplevel) + if window: + window.states = states + + def _handle_done(self, toplevel): + """Handle toplevel done event.""" + # We don't need to print anything here as we're just tracking silently + pass + + def _handle_closed(self, toplevel): + """Handle toplevel closed event.""" + window = self._window_dict.get(toplevel) + if window: + window.closed = True + # Remove from our dictionary + del self._window_dict[toplevel] + + # Clean up the toplevel object + toplevel.destroy() + + def _handle_output_enter(self, toplevel, output): + """Handle toplevel entering an output.""" + window = self._window_dict.get(toplevel) + if window and output not in window.outputs: + window.outputs.append(output) + + def _handle_output_leave(self, toplevel, output): + """Handle toplevel leaving an output.""" + window = self._window_dict.get(toplevel) + if window and output in window.outputs: + window.outputs.remove(output) + + def _handle_parent(self, toplevel, parent): + """Handle toplevel parent changes.""" + window = self._window_dict.get(toplevel) + if window: + if parent is None: + window.parent = None + else: + parent_window = self._window_dict.get(parent) + if parent_window: + window.parent = parent_window + + def _handle_manager_finished(self, manager): + """Handle manager finished event.""" + self.running = False + + def _set_ready(self): + print("IM READY") + self._ready = True + self.ready_signal.emit() + return False + + def get_windows(self) -> List[Window]: + """Get all currently active windows.""" + print([window for window in self._window_dict.values()]) + print("YOU CALLED WINDOWS") + return [window for window in self._window_dict.values() if not window.closed] + + def activate_window(self, window: Window): + if self.seat is None: + print("Cannot activate window: no seat available") + return + + print(f"Activating window: {window.title}") + window.handle.activate(self.seat) + self.display.flush() # flush the request to the Wayland server + + def cleanup(self): + """Clean up resources.""" + self.running = False + print("Cleanup") + + if self.manager: + try: + self.manager.stop() + except: + pass + + # Disconnect from display + if self.display: + try: + self.display.disconnect() + except: + pass + + +def print_windows(tracker): + """Print the current list of windows.""" + windows = tracker.get_windows() + print(f"\nCurrent windows ({len(windows)}):") + if windows: + for i, window in enumerate(windows, 1): + print(f"{i}. {window}") + else: + print("No windows found") diff --git a/flake.lock b/flake.lock index 73e5784..483f28e 100644 --- a/flake.lock +++ b/flake.lock @@ -6,15 +6,15 @@ "utils": "utils" }, "locked": { - "lastModified": 1747045720, - "narHash": "sha256-2Z0F4hnluJZunwRfx80EQXpjGLhunV2wrseT42nzh7M=", - "owner": "Makesesama", + "lastModified": 1745289078, + "narHash": "sha256-1dZTqsWPaHyWjZkX4MaJdwUAQoMXwr8hhHymxQIwFrY=", + "owner": "Fabric-Development", "repo": "fabric", - "rev": "dae50c763e8bf2b4e5807b49b9e62425e0725cfa", + "rev": "1831ced4d9bb9f4be3893be55a8d502b47bff29e", "type": "github" }, "original": { - "owner": "Makesesama", + "owner": "Fabric-Development", "repo": "fabric", "type": "github" } diff --git a/flake.nix b/flake.nix index 97c77c6..b0a6c7a 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,7 @@ nixpkgs.url = "github:NixOS/nixpkgs/24.11"; unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; utils.url = "github:numtide/flake-utils"; - fabric.url = "github:Makesesama/fabric"; + fabric.url = "github:Fabric-Development/fabric"; home-manager.url = "github:nix-community/home-manager"; home-manager.inputs.nixpkgs.follows = "nixpkgs"; }; @@ -35,12 +35,18 @@ packages = { default = pkgs.callPackage ./nix/derivation.nix { inherit (pkgs) lib python3Packages; }; makku = pkgs.writeShellScriptBin "makku" '' - dbus-send --session --print-reply --dest=org.Fabric.fabric.bar /org/Fabric/fabric org.Fabric.fabric.Evaluate string:"finder.show()" > /dev/null 2>&1 + dbus-send --session --print-reply --dest=org.Fabric.fabric.bar /org/Fabric/fabric org.Fabric.fabric.Evaluate string:"finder.open()" > /dev/null 2>&1 ''; }; - apps.default = { - type = "app"; - program = "${self.packages.${system}.default}/bin/bar"; + apps = { + default = { + type = "app"; + program = "${self.packages.${system}.default}/bin/bar"; + }; + show = { + type = "app"; + program = "${self.packages.${system}.makku}/bin/makku"; + }; }; } )