init
This commit is contained in:
3
bar/river/__init__.py
Normal file
3
bar/river/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .service import River
|
||||
|
||||
__all__ = ["River"]
|
||||
85
bar/river/protocol/river-control-unstable-v1.xml
Normal file
85
bar/river/protocol/river-control-unstable-v1.xml
Normal file
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="river_control_unstable_v1">
|
||||
<copyright>
|
||||
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.
|
||||
</copyright>
|
||||
|
||||
<interface name="zriver_control_v1" version="1">
|
||||
<description summary="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.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="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.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="add_argument">
|
||||
<description summary="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.
|
||||
</description>
|
||||
<arg name="argument" type="string" summary="the argument to add"/>
|
||||
</request>
|
||||
|
||||
<request name="run_command">
|
||||
<description summary="run the current command">
|
||||
Execute the command built up using the add_argument request for the
|
||||
given seat.
|
||||
</description>
|
||||
<arg name="seat" type="object" interface="wl_seat"/>
|
||||
<arg name="callback" type="new_id" interface="zriver_command_callback_v1"
|
||||
summary="callback object"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zriver_command_callback_v1" version="1">
|
||||
<description summary="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.
|
||||
</description>
|
||||
|
||||
<event name="success" type="destructor">
|
||||
<description summary="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.
|
||||
</description>
|
||||
<arg name="output" type="string" summary="the output of the command"/>
|
||||
</event>
|
||||
|
||||
<event name="failure" type="destructor">
|
||||
<description summary="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.
|
||||
</description>
|
||||
<arg name="failure_message" type="string"
|
||||
summary="a message explaining why failure occurred"/>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
148
bar/river/protocol/river-status-unstable-v1.xml
Normal file
148
bar/river/protocol/river-status-unstable-v1.xml
Normal file
@@ -0,0 +1,148 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="river_status_unstable_v1">
|
||||
<copyright>
|
||||
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.
|
||||
</copyright>
|
||||
|
||||
<interface name="zriver_status_manager_v1" version="4">
|
||||
<description summary="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.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="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.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="get_river_output_status">
|
||||
<description summary="create an output status object">
|
||||
This creates a new river_output_status object for the given wl_output.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="zriver_output_status_v1"/>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</request>
|
||||
|
||||
<request name="get_river_seat_status">
|
||||
<description summary="create a seat status object">
|
||||
This creates a new river_seat_status object for the given wl_seat.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="zriver_seat_status_v1"/>
|
||||
<arg name="seat" type="object" interface="wl_seat"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zriver_output_status_v1" version="4">
|
||||
<description summary="track output tags and focus">
|
||||
This interface allows clients to receive information about the current
|
||||
windowing state of an output.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_output_status object">
|
||||
This request indicates that the client will not use the
|
||||
river_output_status object any more.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="focused_tags">
|
||||
<description summary="focused tags of the output">
|
||||
Sent once binding the interface and again whenever the tag focus of
|
||||
the output changes.
|
||||
</description>
|
||||
<arg name="tags" type="uint" summary="32-bit bitfield"/>
|
||||
</event>
|
||||
|
||||
<event name="view_tags">
|
||||
<description summary="tag state of an output's views">
|
||||
Sent once on binding the interface and again whenever the tag state
|
||||
of the output changes.
|
||||
</description>
|
||||
<arg name="tags" type="array" summary="array of 32-bit bitfields"/>
|
||||
</event>
|
||||
|
||||
<event name="urgent_tags" since="2">
|
||||
<description summary="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.
|
||||
</description>
|
||||
<arg name="tags" type="uint" summary="32-bit bitfield"/>
|
||||
</event>
|
||||
|
||||
<event name="layout_name" since="4">
|
||||
<description summary="name of the layout">
|
||||
Sent once on binding the interface should a layout name exist and again
|
||||
whenever the name changes.
|
||||
</description>
|
||||
<arg name="name" type="string" summary="layout name"/>
|
||||
</event>
|
||||
|
||||
<event name="layout_name_clear" since="4">
|
||||
<description summary="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.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
<interface name="zriver_seat_status_v1" version="3">
|
||||
<description summary="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 wl_output globals.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_seat_status object">
|
||||
This request indicates that the client will not use the
|
||||
river_seat_status object any more.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="focused_output">
|
||||
<description summary="the seat focused an output">
|
||||
Sent on binding the interface and again whenever an output gains focus.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</event>
|
||||
|
||||
<event name="unfocused_output">
|
||||
<description summary="the seat unfocused an output">
|
||||
Sent whenever an output loses focus.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</event>
|
||||
|
||||
<event name="focused_view">
|
||||
<description summary="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.
|
||||
</description>
|
||||
<arg name="title" type="string" summary="title of the focused view"/>
|
||||
</event>
|
||||
|
||||
<event name="mode" since="3">
|
||||
<description summary="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).
|
||||
</description>
|
||||
<arg name="name" type="string" summary="name of the mode"/>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
117
bar/river/service.py
Normal file
117
bar/river/service.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List
|
||||
|
||||
from fabric.core.service import Service, Signal, Property
|
||||
from pywayland.client import Display
|
||||
from pywayland.protocol.wayland import WlOutput, WlSeat
|
||||
from ..generated.river_status_unstable_v1 import (
|
||||
ZriverStatusManagerV1,
|
||||
ZriverOutputStatusV1,
|
||||
ZriverSeatStatusV1,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputState:
|
||||
id: int
|
||||
output: WlOutput
|
||||
status: ZriverOutputStatusV1 = None
|
||||
focused_tags: List[int] = field(default_factory=list)
|
||||
view_tags: List[int] = field(default_factory=list)
|
||||
|
||||
|
||||
class River(Service):
|
||||
@Property(bool, "readable", "is-ready", default_value=False)
|
||||
def ready(self) -> bool:
|
||||
return self._ready
|
||||
|
||||
@Signal
|
||||
def ready(self):
|
||||
return self.notify("ready")
|
||||
|
||||
@Signal("event", flags="detailed")
|
||||
def event(self, event: object): ...
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._ready = False
|
||||
self.display = None
|
||||
self.registry = None
|
||||
self.outputs: Dict[int, OutputState] = {}
|
||||
self.status_mgr = None
|
||||
self.seat = None
|
||||
self.seat_status: ZriverSeatStatusV1 = None
|
||||
self.ready.emit()
|
||||
|
||||
def on_start(self):
|
||||
print("✅ River.on_start called")
|
||||
|
||||
logger.info("[RiverService] Starting River service...")
|
||||
self.display = Display()
|
||||
self.display.connect()
|
||||
self.registry = self.display.get_registry()
|
||||
|
||||
self.registry.dispatcher["global"] = self._on_global
|
||||
self.registry.dispatcher["global_remove"] = lambda *_: None
|
||||
|
||||
self.display.roundtrip()
|
||||
|
||||
if self.seat and self.status_mgr:
|
||||
self.seat_status = self.status_mgr.get_river_seat_status(self.seat)
|
||||
|
||||
for id, output_state in self.outputs.items():
|
||||
status = self.status_mgr.get_river_output_status(output_state.output)
|
||||
output_state.status = status
|
||||
status.dispatcher["focused_tags"] = self._make_focused_handler(id)
|
||||
status.dispatcher["view_tags"] = self._make_view_handler(id)
|
||||
|
||||
self.display.roundtrip()
|
||||
|
||||
self._ready = True
|
||||
self.ready.emit()
|
||||
logger.info("[RiverService] Ready. Monitoring tags.")
|
||||
|
||||
def on_tick(self):
|
||||
# Periodic poll
|
||||
self.display.roundtrip()
|
||||
|
||||
def _on_global(self, registry, name, interface, version):
|
||||
if interface == "wl_output":
|
||||
output = registry.bind(name, WlOutput, version)
|
||||
self.outputs[name] = OutputState(id=name, output=output)
|
||||
|
||||
elif interface == "wl_seat":
|
||||
self.seat = registry.bind(name, WlSeat, version)
|
||||
|
||||
elif interface == "zriver_status_manager_v1":
|
||||
self.status_mgr = registry.bind(name, ZriverStatusManagerV1, version)
|
||||
|
||||
def _make_focused_handler(self, output_id):
|
||||
def handler(_, bitfield):
|
||||
tags = self._decode_bitfield(bitfield)
|
||||
self.outputs[output_id].focused_tags = tags
|
||||
logger.debug(f"[RiverService] Output {output_id} focused: {tags}")
|
||||
self.emit(f"event::focused_tags::{output_id}", tags)
|
||||
|
||||
return handler
|
||||
|
||||
def _make_view_handler(self, output_id):
|
||||
def handler(_, array):
|
||||
tags = self._decode_array_bitfields(array)
|
||||
self.outputs[output_id].view_tags = tags
|
||||
logger.debug(f"[RiverService] Output {output_id} views: {tags}")
|
||||
self.emit(f"event::view_tags::{output_id}", tags)
|
||||
|
||||
return handler
|
||||
|
||||
def _decode_bitfield(self, bits: int) -> List[int]:
|
||||
return [i for i in range(32) if bits & (1 << i)]
|
||||
|
||||
def _decode_array_bitfields(self, array) -> List[int]:
|
||||
tags = set()
|
||||
for bits in array:
|
||||
tags.update(self._decode_bitfield(bits))
|
||||
return sorted(tags)
|
||||
99
bar/river/widgets.py
Normal file
99
bar/river/widgets.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from fabric.core.service import Property
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.utils.helpers import bulk_connect
|
||||
from .service import River
|
||||
|
||||
|
||||
from gi.repository import Gdk
|
||||
|
||||
_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")
|
||||
|
||||
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
|
||||
|
||||
|
||||
class RiverWorkspaces(EventBox):
|
||||
def __init__(self, output_id: int, max_tags: int = 9, **kwargs):
|
||||
super().__init__(events="scroll")
|
||||
self.output_id = output_id
|
||||
self.max_tags = max_tags
|
||||
self.service = get_river_connection()
|
||||
self._box = Box(**kwargs)
|
||||
self.children = self._box
|
||||
|
||||
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)
|
||||
|
||||
# hook into River signals
|
||||
self.service.connect(f"event::focused_tags::{output_id}", self.on_focus_change)
|
||||
self.service.connect(f"event::view_tags::{output_id}", self.on_view_change)
|
||||
if self.service.ready:
|
||||
self.on_ready(None)
|
||||
else:
|
||||
self.service.connect("event::ready", self.on_ready)
|
||||
|
||||
self.connect("scroll-event", self.on_scroll)
|
||||
|
||||
def on_ready(self, _):
|
||||
print(self.service.outputs)
|
||||
|
||||
def on_focus_change(self, _, tags: list[int]):
|
||||
print(tags)
|
||||
for i, btn in self._buttons.items():
|
||||
btn.active = i in tags
|
||||
|
||||
def on_view_change(self, _, tags: list[int]):
|
||||
print(tags)
|
||||
for i, btn in self._buttons.items():
|
||||
btn.empty = i not in tags
|
||||
|
||||
def on_workspace_click(self, btn: RiverWorkspaceButton):
|
||||
import subprocess
|
||||
|
||||
subprocess.run(["riverctl", "tag", str(btn.id)])
|
||||
return
|
||||
|
||||
def on_scroll(self, _, event: Gdk.EventScroll):
|
||||
direction = event.direction # UP or DOWN
|
||||
cmd = "tag +1" if direction == Gdk.ScrollDirection.DOWN else "tag -1"
|
||||
import subprocess
|
||||
|
||||
subprocess.run(["riverctl", *cmd.split()])
|
||||
Reference in New Issue
Block a user