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";
+ };
};
}
)