mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-15 00:32:47 -04:00
workspace/ext-ws: drop custom ext-workspace in favor of quickshell
WindowManager implementation
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,422 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<protocol name="ext_workspace_v1">
|
|
||||||
<copyright>
|
|
||||||
Copyright © 2019 Christopher Billington
|
|
||||||
Copyright © 2020 Ilia Bozhinov
|
|
||||||
Copyright © 2022 Victoria Brekenfeld
|
|
||||||
|
|
||||||
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.
|
|
||||||
</copyright>
|
|
||||||
|
|
||||||
<interface name="ext_workspace_manager_v1" version="1">
|
|
||||||
<description summary="list and control workspaces">
|
|
||||||
Workspaces, also called virtual desktops, are groups of surfaces. A
|
|
||||||
compositor with a concept of workspaces may only show some such groups of
|
|
||||||
surfaces (those of 'active' workspaces) at a time. 'Activating' a
|
|
||||||
workspace is a request for the compositor to display that workspace's
|
|
||||||
surfaces as normal, whereas the compositor may hide or otherwise
|
|
||||||
de-emphasise surfaces that are associated only with 'inactive' workspaces.
|
|
||||||
Workspaces are grouped by which sets of outputs they correspond to, and
|
|
||||||
may contain surfaces only from those outputs. In this way, it is possible
|
|
||||||
for each output to have its own set of workspaces, or for all outputs (or
|
|
||||||
any other arbitrary grouping) to share workspaces. Compositors may
|
|
||||||
optionally conceptually arrange each group of workspaces in an
|
|
||||||
N-dimensional grid.
|
|
||||||
|
|
||||||
The purpose of this protocol is to enable the creation of taskbars and
|
|
||||||
docks by providing them with a list of workspaces and their properties,
|
|
||||||
and allowing them to activate and deactivate workspaces.
|
|
||||||
|
|
||||||
After a client binds the ext_workspace_manager_v1, each workspace will be
|
|
||||||
sent via the workspace event.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<event name="workspace_group">
|
|
||||||
<description summary="a workspace group has been created">
|
|
||||||
This event is emitted whenever a new workspace group has been created.
|
|
||||||
|
|
||||||
All initial details of the workspace group (outputs) will be
|
|
||||||
sent immediately after this event via the corresponding events in
|
|
||||||
ext_workspace_group_handle_v1 and ext_workspace_handle_v1.
|
|
||||||
</description>
|
|
||||||
<arg name="workspace_group" type="new_id" interface="ext_workspace_group_handle_v1"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="workspace">
|
|
||||||
<description summary="workspace has been created">
|
|
||||||
This event is emitted whenever a new workspace has been created.
|
|
||||||
|
|
||||||
All initial details of the workspace (name, coordinates, state) will
|
|
||||||
be sent immediately after this event via the corresponding events in
|
|
||||||
ext_workspace_handle_v1.
|
|
||||||
|
|
||||||
Workspaces start off unassigned to any workspace group.
|
|
||||||
</description>
|
|
||||||
<arg name="workspace" type="new_id" interface="ext_workspace_handle_v1"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<request name="commit">
|
|
||||||
<description summary="all requests about the workspaces have been sent">
|
|
||||||
The client must send this request after it has finished sending other
|
|
||||||
requests. The compositor must process a series of requests preceding a
|
|
||||||
commit request atomically.
|
|
||||||
|
|
||||||
This allows changes to the workspace properties to be seen as atomic,
|
|
||||||
even if they happen via multiple events, and even if they involve
|
|
||||||
multiple ext_workspace_handle_v1 objects, for example, deactivating one
|
|
||||||
workspace and activating another.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<event name="done">
|
|
||||||
<description summary="all information about the workspaces and workspace groups has been sent">
|
|
||||||
This event is sent after all changes in all workspaces and workspace groups have been
|
|
||||||
sent.
|
|
||||||
|
|
||||||
This allows changes to one or more ext_workspace_group_handle_v1
|
|
||||||
properties and ext_workspace_handle_v1 properties
|
|
||||||
to be seen as atomic, even if they happen via multiple events.
|
|
||||||
In particular, an output moving from one workspace group to
|
|
||||||
another sends an output_enter event and an output_leave event to the two
|
|
||||||
ext_workspace_group_handle_v1 objects in question. The compositor sends
|
|
||||||
the done event only after updating the output information in both
|
|
||||||
workspace groups.
|
|
||||||
</description>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="finished" type="destructor">
|
|
||||||
<description summary="the compositor has finished with the workspace_manager">
|
|
||||||
This event indicates that the compositor is done sending events to the
|
|
||||||
ext_workspace_manager_v1. The server will destroy the object
|
|
||||||
immediately after sending this request.
|
|
||||||
</description>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<request name="stop">
|
|
||||||
<description summary="stop sending events">
|
|
||||||
Indicates the client no longer wishes to receive events for new
|
|
||||||
workspace groups. However the compositor may emit further workspace
|
|
||||||
events, until the finished event is emitted. The compositor is expected
|
|
||||||
to send the finished event eventually once the stop request has been processed.
|
|
||||||
|
|
||||||
The client must not send any requests after this one, doing so will raise a wl_display
|
|
||||||
invalid_object error.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
</interface>
|
|
||||||
|
|
||||||
<interface name="ext_workspace_group_handle_v1" version="1">
|
|
||||||
<description summary="a workspace group assigned to a set of outputs">
|
|
||||||
A ext_workspace_group_handle_v1 object represents a workspace group
|
|
||||||
that is assigned a set of outputs and contains a number of workspaces.
|
|
||||||
|
|
||||||
The set of outputs assigned to the workspace group is conveyed to the client via
|
|
||||||
output_enter and output_leave events, and its workspaces are conveyed with
|
|
||||||
workspace events.
|
|
||||||
|
|
||||||
For example, a compositor which has a set of workspaces for each output may
|
|
||||||
advertise a workspace group (and its workspaces) per output, whereas a compositor
|
|
||||||
where a workspace spans all outputs may advertise a single workspace group for all
|
|
||||||
outputs.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<enum name="group_capabilities" bitfield="true">
|
|
||||||
<entry name="create_workspace" value="1" summary="create_workspace request is available"/>
|
|
||||||
</enum>
|
|
||||||
|
|
||||||
<event name="capabilities">
|
|
||||||
<description summary="compositor capabilities">
|
|
||||||
This event advertises the capabilities supported by the compositor. If
|
|
||||||
a capability isn't supported, clients should hide or disable the UI
|
|
||||||
elements that expose this functionality. For instance, if the
|
|
||||||
compositor doesn't advertise support for creating workspaces, a button
|
|
||||||
triggering the create_workspace request should not be displayed.
|
|
||||||
|
|
||||||
The compositor will ignore requests it doesn't support. For instance,
|
|
||||||
a compositor which doesn't advertise support for creating workspaces will ignore
|
|
||||||
create_workspace requests.
|
|
||||||
|
|
||||||
Compositors must send this event once after creation of an
|
|
||||||
ext_workspace_group_handle_v1. When the capabilities change, compositors
|
|
||||||
must send this event again.
|
|
||||||
</description>
|
|
||||||
<arg name="capabilities" type="uint" summary="capabilities" enum="group_capabilities"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="output_enter">
|
|
||||||
<description summary="output assigned to workspace group">
|
|
||||||
This event is emitted whenever an output is assigned to the workspace
|
|
||||||
group or a new `wl_output` object is bound by the client, which was already
|
|
||||||
assigned to this workspace_group.
|
|
||||||
</description>
|
|
||||||
<arg name="output" type="object" interface="wl_output"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="output_leave">
|
|
||||||
<description summary="output removed from workspace group">
|
|
||||||
This event is emitted whenever an output is removed from the workspace
|
|
||||||
group.
|
|
||||||
</description>
|
|
||||||
<arg name="output" type="object" interface="wl_output"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="workspace_enter">
|
|
||||||
<description summary="workspace added to workspace group">
|
|
||||||
This event is emitted whenever a workspace is assigned to this group.
|
|
||||||
A workspace may only ever be assigned to a single group at a single point
|
|
||||||
in time, but can be re-assigned during it's lifetime.
|
|
||||||
</description>
|
|
||||||
<arg name="workspace" type="object" interface="ext_workspace_handle_v1"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="workspace_leave">
|
|
||||||
<description summary="workspace removed from workspace group">
|
|
||||||
This event is emitted whenever a workspace is removed from this group.
|
|
||||||
</description>
|
|
||||||
<arg name="workspace" type="object" interface="ext_workspace_handle_v1"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="removed">
|
|
||||||
<description summary="this workspace group has been removed">
|
|
||||||
This event is send when the group associated with the ext_workspace_group_handle_v1
|
|
||||||
has been removed. After sending this request the compositor will immediately consider
|
|
||||||
the object inert. Any requests will be ignored except the destroy request.
|
|
||||||
It is guaranteed there won't be any more events referencing this
|
|
||||||
ext_workspace_group_handle_v1.
|
|
||||||
|
|
||||||
The compositor must remove all workspaces belonging to a workspace group
|
|
||||||
via a workspace_leave event before removing the workspace group.
|
|
||||||
</description>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<request name="create_workspace">
|
|
||||||
<description summary="create a new workspace">
|
|
||||||
Request that the compositor create a new workspace with the given name
|
|
||||||
and assign it to this group.
|
|
||||||
|
|
||||||
There is no guarantee that the compositor will create a new workspace,
|
|
||||||
or that the created workspace will have the provided name.
|
|
||||||
</description>
|
|
||||||
<arg name="workspace" type="string"/>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="destroy" type="destructor">
|
|
||||||
<description summary="destroy the ext_workspace_group_handle_v1 object">
|
|
||||||
Destroys the ext_workspace_group_handle_v1 object.
|
|
||||||
|
|
||||||
This request should be send either when the client does not want to
|
|
||||||
use the workspace group object any more or after the removed event to finalize
|
|
||||||
the destruction of the object.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
</interface>
|
|
||||||
|
|
||||||
<interface name="ext_workspace_handle_v1" version="1">
|
|
||||||
<description summary="a workspace handing a group of surfaces">
|
|
||||||
A ext_workspace_handle_v1 object represents a workspace that handles a
|
|
||||||
group of surfaces.
|
|
||||||
|
|
||||||
Each workspace has:
|
|
||||||
- a name, conveyed to the client with the name event
|
|
||||||
- potentially an id conveyed with the id event
|
|
||||||
- a list of states, conveyed to the client with the state event
|
|
||||||
- and optionally a set of coordinates, conveyed to the client with the
|
|
||||||
coordinates event
|
|
||||||
|
|
||||||
The client may request that the compositor activate or deactivate the workspace.
|
|
||||||
|
|
||||||
Each workspace can belong to only a single workspace group.
|
|
||||||
Depending on the compositor policy, there might be workspaces with
|
|
||||||
the same name in different workspace groups, but these workspaces are still
|
|
||||||
separate (e.g. one of them might be active while the other is not).
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<event name="id">
|
|
||||||
<description summary="workspace id">
|
|
||||||
If this event is emitted, it will be send immediately after the
|
|
||||||
ext_workspace_handle_v1 is created or when an id is assigned to
|
|
||||||
a workspace (at most once during it's lifetime).
|
|
||||||
|
|
||||||
An id will never change during the lifetime of the `ext_workspace_handle_v1`
|
|
||||||
and is guaranteed to be unique during it's lifetime.
|
|
||||||
|
|
||||||
Ids are not human-readable and shouldn't be displayed, use `name` for that purpose.
|
|
||||||
|
|
||||||
Compositors are expected to only send ids for workspaces likely stable across multiple
|
|
||||||
sessions and can be used by clients to store preferences for workspaces. Workspaces without
|
|
||||||
ids should be considered temporary and any data associated with them should be deleted once
|
|
||||||
the respective object is lost.
|
|
||||||
</description>
|
|
||||||
<arg name="id" type="string"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="name">
|
|
||||||
<description summary="workspace name changed">
|
|
||||||
This event is emitted immediately after the ext_workspace_handle_v1 is
|
|
||||||
created and whenever the name of the workspace changes.
|
|
||||||
|
|
||||||
A name is meant to be human-readable and can be displayed to a user.
|
|
||||||
Unlike the id it is neither stable nor unique.
|
|
||||||
</description>
|
|
||||||
<arg name="name" type="string"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="coordinates">
|
|
||||||
<description summary="workspace coordinates changed">
|
|
||||||
This event is used to organize workspaces into an N-dimensional grid
|
|
||||||
within a workspace group, and if supported, is emitted immediately after
|
|
||||||
the ext_workspace_handle_v1 is created and whenever the coordinates of
|
|
||||||
the workspace change. Compositors may not send this event if they do not
|
|
||||||
conceptually arrange workspaces in this way. If compositors simply
|
|
||||||
number workspaces, without any geometric interpretation, they may send
|
|
||||||
1D coordinates, which clients should not interpret as implying any
|
|
||||||
geometry. Sending an empty array means that the compositor no longer
|
|
||||||
orders the workspace geometrically.
|
|
||||||
|
|
||||||
Coordinates have an arbitrary number of dimensions N with an uint32
|
|
||||||
position along each dimension. By convention if N > 1, the first
|
|
||||||
dimension is X, the second Y, the third Z, and so on. The compositor may
|
|
||||||
chose to utilize these events for a more novel workspace layout
|
|
||||||
convention, however. No guarantee is made about the grid being filled or
|
|
||||||
bounded; there may be a workspace at coordinate 1 and another at
|
|
||||||
coordinate 1000 and none in between. Within a workspace group, however,
|
|
||||||
workspaces must have unique coordinates of equal dimensionality.
|
|
||||||
</description>
|
|
||||||
<arg name="coordinates" type="array"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<enum name="state" bitfield="true">
|
|
||||||
<description summary="types of states on the workspace">
|
|
||||||
The different states that a workspace can have.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<entry name="active" value="1" summary="the workspace is active"/>
|
|
||||||
<entry name="urgent" value="2" summary="the workspace requests attention"/>
|
|
||||||
<entry name="hidden" value="4">
|
|
||||||
<description summary="the workspace is not visible">
|
|
||||||
The workspace is not visible in its workspace group, and clients
|
|
||||||
attempting to visualize the compositor workspace state should not
|
|
||||||
display such workspaces.
|
|
||||||
</description>
|
|
||||||
</entry>
|
|
||||||
</enum>
|
|
||||||
|
|
||||||
<event name="state">
|
|
||||||
<description summary="the state of the workspace changed">
|
|
||||||
This event is emitted immediately after the ext_workspace_handle_v1 is
|
|
||||||
created and each time the workspace state changes, either because of a
|
|
||||||
compositor action or because of a request in this protocol.
|
|
||||||
|
|
||||||
Missing states convey the opposite meaning, e.g. an unset active bit
|
|
||||||
means the workspace is currently inactive.
|
|
||||||
</description>
|
|
||||||
<arg name="state" type="uint" enum="state"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<enum name="workspace_capabilities" bitfield="true">
|
|
||||||
<entry name="activate" value="1" summary="activate request is available"/>
|
|
||||||
<entry name="deactivate" value="2" summary="deactivate request is available"/>
|
|
||||||
<entry name="remove" value="4" summary="remove request is available"/>
|
|
||||||
<entry name="assign" value="8" summary="assign request is available"/>
|
|
||||||
</enum>
|
|
||||||
|
|
||||||
<event name="capabilities">
|
|
||||||
<description summary="compositor capabilities">
|
|
||||||
This event advertises the capabilities supported by the compositor. If
|
|
||||||
a capability isn't supported, clients should hide or disable the UI
|
|
||||||
elements that expose this functionality. For instance, if the
|
|
||||||
compositor doesn't advertise support for removing workspaces, a button
|
|
||||||
triggering the remove request should not be displayed.
|
|
||||||
|
|
||||||
The compositor will ignore requests it doesn't support. For instance,
|
|
||||||
a compositor which doesn't advertise support for remove will ignore
|
|
||||||
remove requests.
|
|
||||||
|
|
||||||
Compositors must send this event once after creation of an
|
|
||||||
ext_workspace_handle_v1 . When the capabilities change, compositors
|
|
||||||
must send this event again.
|
|
||||||
</description>
|
|
||||||
<arg name="capabilities" type="uint" summary="capabilities" enum="workspace_capabilities"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="removed">
|
|
||||||
<description summary="this workspace has been removed">
|
|
||||||
This event is send when the workspace associated with the ext_workspace_handle_v1
|
|
||||||
has been removed. After sending this request, the compositor will immediately consider
|
|
||||||
the object inert. Any requests will be ignored except the destroy request.
|
|
||||||
|
|
||||||
It is guaranteed there won't be any more events referencing this
|
|
||||||
ext_workspace_handle_v1.
|
|
||||||
|
|
||||||
The compositor must only remove a workspaces not currently belonging to any
|
|
||||||
workspace_group.
|
|
||||||
</description>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<request name="destroy" type="destructor">
|
|
||||||
<description summary="destroy the ext_workspace_handle_v1 object">
|
|
||||||
Destroys the ext_workspace_handle_v1 object.
|
|
||||||
|
|
||||||
This request should be made either when the client does not want to
|
|
||||||
use the workspace object any more or after the remove event to finalize
|
|
||||||
the destruction of the object.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="activate">
|
|
||||||
<description summary="activate the workspace">
|
|
||||||
Request that this workspace be activated.
|
|
||||||
|
|
||||||
There is no guarantee the workspace will be actually activated, and
|
|
||||||
behaviour may be compositor-dependent. For example, activating a
|
|
||||||
workspace may or may not deactivate all other workspaces in the same
|
|
||||||
group.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="deactivate">
|
|
||||||
<description summary="deactivate the workspace">
|
|
||||||
Request that this workspace be deactivated.
|
|
||||||
|
|
||||||
There is no guarantee the workspace will be actually deactivated.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="assign">
|
|
||||||
<description summary="assign workspace to group">
|
|
||||||
Requests that this workspace is assigned to the given workspace group.
|
|
||||||
|
|
||||||
There is no guarantee the workspace will be assigned.
|
|
||||||
</description>
|
|
||||||
<arg name="workspace_group" type="object" interface="ext_workspace_group_handle_v1"/>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="remove">
|
|
||||||
<description summary="remove the workspace">
|
|
||||||
Request that this workspace be removed.
|
|
||||||
|
|
||||||
There is no guarantee the workspace will be actually removed.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
</interface>
|
|
||||||
</protocol>
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package extworkspace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SuccessResult struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
|
||||||
if manager == nil {
|
|
||||||
models.RespondError(conn, req.ID, "extworkspace manager not initialized")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch req.Method {
|
|
||||||
case "extworkspace.getState":
|
|
||||||
handleGetState(conn, req, manager)
|
|
||||||
case "extworkspace.activateWorkspace":
|
|
||||||
handleActivateWorkspace(conn, req, manager)
|
|
||||||
case "extworkspace.deactivateWorkspace":
|
|
||||||
handleDeactivateWorkspace(conn, req, manager)
|
|
||||||
case "extworkspace.removeWorkspace":
|
|
||||||
handleRemoveWorkspace(conn, req, manager)
|
|
||||||
case "extworkspace.createWorkspace":
|
|
||||||
handleCreateWorkspace(conn, req, manager)
|
|
||||||
case "extworkspace.subscribe":
|
|
||||||
handleSubscribe(conn, req, manager)
|
|
||||||
default:
|
|
||||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
|
||||||
state := manager.GetState()
|
|
||||||
models.Respond(conn, req.ID, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleActivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
|
||||||
groupID := models.GetOr(req, "groupID", "")
|
|
||||||
workspaceID, ok := models.Get[string](req, "workspaceID")
|
|
||||||
if !ok {
|
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.ActivateWorkspace(groupID, workspaceID); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace activated"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleDeactivateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
|
||||||
groupID := models.GetOr(req, "groupID", "")
|
|
||||||
workspaceID, ok := models.Get[string](req, "workspaceID")
|
|
||||||
if !ok {
|
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.DeactivateWorkspace(groupID, workspaceID); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace deactivated"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRemoveWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
|
||||||
groupID := models.GetOr(req, "groupID", "")
|
|
||||||
workspaceID, ok := models.Get[string](req, "workspaceID")
|
|
||||||
if !ok {
|
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'workspaceID' parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.RemoveWorkspace(groupID, workspaceID); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace removed"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCreateWorkspace(conn net.Conn, req models.Request, manager *Manager) {
|
|
||||||
groupID, ok := models.Get[string](req, "groupID")
|
|
||||||
if !ok {
|
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'groupID' parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceName, ok := models.Get[string](req, "name")
|
|
||||||
if !ok {
|
|
||||||
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.CreateWorkspace(groupID, workspaceName); err != nil {
|
|
||||||
models.RespondError(conn, req.ID, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "workspace create requested"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
|
||||||
clientID := fmt.Sprintf("client-%p", conn)
|
|
||||||
stateChan := manager.Subscribe(clientID)
|
|
||||||
defer manager.Unsubscribe(clientID)
|
|
||||||
|
|
||||||
initialState := manager.GetState()
|
|
||||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
|
||||||
ID: req.ID,
|
|
||||||
Result: &initialState,
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for state := range stateChan {
|
|
||||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
|
||||||
Result: &state,
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,598 +0,0 @@
|
|||||||
package extworkspace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CheckCapability() bool {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer display.Destroy()
|
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer registry.Destroy()
|
|
||||||
|
|
||||||
found := false
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Roundtrip to ensure all registry events are processed
|
|
||||||
if err := display.Roundtrip(); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
|
|
||||||
m := &Manager{
|
|
||||||
display: display,
|
|
||||||
ctx: display.Context(),
|
|
||||||
cmdq: make(chan cmd, 128),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.wg.Add(1)
|
|
||||||
go m.waylandActor()
|
|
||||||
|
|
||||||
if err := m.setupRegistry(); err != nil {
|
|
||||||
close(m.stopChan)
|
|
||||||
m.wg.Wait()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.updateState()
|
|
||||||
|
|
||||||
m.notifierWg.Add(1)
|
|
||||||
go m.notifier()
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) post(fn func()) {
|
|
||||||
select {
|
|
||||||
case m.cmdq <- cmd{fn: fn}:
|
|
||||||
default:
|
|
||||||
log.Warn("ExtWorkspace actor command queue full, dropping command")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) waylandActor() {
|
|
||||||
defer m.wg.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-m.stopChan:
|
|
||||||
return
|
|
||||||
case c := <-m.cmdq:
|
|
||||||
c.fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) setupRegistry() error {
|
|
||||||
log.Info("ExtWorkspace: starting registry setup")
|
|
||||||
|
|
||||||
registry, err := m.display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get registry: %w", err)
|
|
||||||
}
|
|
||||||
m.registry = registry
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
if e.Interface == "wl_output" {
|
|
||||||
output := wlclient.NewOutput(m.ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, 4, output); err == nil {
|
|
||||||
outputID := output.ID()
|
|
||||||
|
|
||||||
output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
|
|
||||||
m.outputNames.Store(outputID, ev.Name)
|
|
||||||
log.Debugf("ExtWorkspace: Output %d (%s) name received", outputID, ev.Name)
|
|
||||||
m.post(func() {
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Interface == ext_workspace.ExtWorkspaceManagerV1InterfaceName {
|
|
||||||
log.Infof("ExtWorkspace: found %s", ext_workspace.ExtWorkspaceManagerV1InterfaceName)
|
|
||||||
manager := ext_workspace.NewExtWorkspaceManagerV1(m.ctx)
|
|
||||||
version := e.Version
|
|
||||||
if version > 1 {
|
|
||||||
version = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.SetWorkspaceGroupHandler(func(e ext_workspace.ExtWorkspaceManagerV1WorkspaceGroupEvent) {
|
|
||||||
m.handleWorkspaceGroup(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
manager.SetWorkspaceHandler(func(e ext_workspace.ExtWorkspaceManagerV1WorkspaceEvent) {
|
|
||||||
m.handleWorkspace(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
manager.SetDoneHandler(func(e ext_workspace.ExtWorkspaceManagerV1DoneEvent) {
|
|
||||||
log.Debug("ExtWorkspace: done event received")
|
|
||||||
m.post(func() {
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
manager.SetFinishedHandler(func(e ext_workspace.ExtWorkspaceManagerV1FinishedEvent) {
|
|
||||||
log.Info("ExtWorkspace: finished event received")
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil {
|
|
||||||
m.manager = manager
|
|
||||||
log.Info("ExtWorkspace: manager bound successfully")
|
|
||||||
} else {
|
|
||||||
log.Errorf("ExtWorkspace: failed to bind manager: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Info("ExtWorkspace: registry setup complete (events will be processed async)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) handleWorkspaceGroup(e ext_workspace.ExtWorkspaceManagerV1WorkspaceGroupEvent) {
|
|
||||||
handle := e.WorkspaceGroup
|
|
||||||
groupID := handle.ID()
|
|
||||||
|
|
||||||
log.Debugf("ExtWorkspace: New workspace group (id=%d)", groupID)
|
|
||||||
|
|
||||||
group := &workspaceGroupState{
|
|
||||||
id: groupID,
|
|
||||||
handle: handle,
|
|
||||||
outputIDs: make(map[uint32]bool),
|
|
||||||
workspaceIDs: make([]uint32, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.groups.Store(groupID, group)
|
|
||||||
|
|
||||||
handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1CapabilitiesEvent) {
|
|
||||||
log.Debugf("ExtWorkspace: Group %d capabilities: %d", groupID, e.Capabilities)
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetOutputEnterHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1OutputEnterEvent) {
|
|
||||||
outputID := e.Output.ID()
|
|
||||||
log.Debugf("ExtWorkspace: Group %d output enter (output=%d)", groupID, outputID)
|
|
||||||
|
|
||||||
m.post(func() {
|
|
||||||
group.outputIDs[outputID] = true
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetOutputLeaveHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1OutputLeaveEvent) {
|
|
||||||
outputID := e.Output.ID()
|
|
||||||
log.Debugf("ExtWorkspace: Group %d output leave (output=%d)", groupID, outputID)
|
|
||||||
m.post(func() {
|
|
||||||
delete(group.outputIDs, outputID)
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetWorkspaceEnterHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1WorkspaceEnterEvent) {
|
|
||||||
workspaceID := e.Workspace.ID()
|
|
||||||
log.Debugf("ExtWorkspace: Group %d workspace enter (workspace=%d)", groupID, workspaceID)
|
|
||||||
|
|
||||||
m.post(func() {
|
|
||||||
if ws, ok := m.workspaces.Load(workspaceID); ok {
|
|
||||||
ws.groupID = groupID
|
|
||||||
}
|
|
||||||
|
|
||||||
group.workspaceIDs = append(group.workspaceIDs, workspaceID)
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetWorkspaceLeaveHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1WorkspaceLeaveEvent) {
|
|
||||||
workspaceID := e.Workspace.ID()
|
|
||||||
log.Debugf("ExtWorkspace: Group %d workspace leave (workspace=%d)", groupID, workspaceID)
|
|
||||||
|
|
||||||
m.post(func() {
|
|
||||||
if ws, ok := m.workspaces.Load(workspaceID); ok {
|
|
||||||
ws.groupID = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, id := range group.workspaceIDs {
|
|
||||||
if id == workspaceID {
|
|
||||||
group.workspaceIDs = append(group.workspaceIDs[:i], group.workspaceIDs[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceGroupHandleV1RemovedEvent) {
|
|
||||||
log.Debugf("ExtWorkspace: Group %d removed", groupID)
|
|
||||||
|
|
||||||
m.post(func() {
|
|
||||||
group.removed = true
|
|
||||||
|
|
||||||
m.groups.Delete(groupID)
|
|
||||||
|
|
||||||
m.wlMutex.Lock()
|
|
||||||
handle.Destroy()
|
|
||||||
m.wlMutex.Unlock()
|
|
||||||
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) handleWorkspace(e ext_workspace.ExtWorkspaceManagerV1WorkspaceEvent) {
|
|
||||||
handle := e.Workspace
|
|
||||||
workspaceID := handle.ID()
|
|
||||||
|
|
||||||
log.Debugf("ExtWorkspace: New workspace (proxy_id=%d)", workspaceID)
|
|
||||||
|
|
||||||
ws := &workspaceState{
|
|
||||||
id: workspaceID,
|
|
||||||
handle: handle,
|
|
||||||
coordinates: make([]uint32, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.workspaces.Store(workspaceID, ws)
|
|
||||||
|
|
||||||
handle.SetIdHandler(func(e ext_workspace.ExtWorkspaceHandleV1IdEvent) {
|
|
||||||
log.Debugf("ExtWorkspace: Workspace %d id: %s", workspaceID, e.Id)
|
|
||||||
m.post(func() {
|
|
||||||
ws.workspaceID = e.Id
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetNameHandler(func(e ext_workspace.ExtWorkspaceHandleV1NameEvent) {
|
|
||||||
log.Debugf("ExtWorkspace: Workspace %d name: %s", workspaceID, e.Name)
|
|
||||||
m.post(func() {
|
|
||||||
ws.name = e.Name
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetCoordinatesHandler(func(e ext_workspace.ExtWorkspaceHandleV1CoordinatesEvent) {
|
|
||||||
coords := make([]uint32, 0)
|
|
||||||
for i := 0; i < len(e.Coordinates); i += 4 {
|
|
||||||
if i+4 <= len(e.Coordinates) {
|
|
||||||
val := uint32(e.Coordinates[i]) |
|
|
||||||
uint32(e.Coordinates[i+1])<<8 |
|
|
||||||
uint32(e.Coordinates[i+2])<<16 |
|
|
||||||
uint32(e.Coordinates[i+3])<<24
|
|
||||||
coords = append(coords, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Debugf("ExtWorkspace: Workspace %d coordinates: %v", workspaceID, coords)
|
|
||||||
m.post(func() {
|
|
||||||
ws.coordinates = coords
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetStateHandler(func(e ext_workspace.ExtWorkspaceHandleV1StateEvent) {
|
|
||||||
log.Debugf("ExtWorkspace: Workspace %d state: %d", workspaceID, e.State)
|
|
||||||
m.post(func() {
|
|
||||||
ws.state = e.State
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetCapabilitiesHandler(func(e ext_workspace.ExtWorkspaceHandleV1CapabilitiesEvent) {
|
|
||||||
log.Debugf("ExtWorkspace: Workspace %d capabilities: %d", workspaceID, e.Capabilities)
|
|
||||||
})
|
|
||||||
|
|
||||||
handle.SetRemovedHandler(func(e ext_workspace.ExtWorkspaceHandleV1RemovedEvent) {
|
|
||||||
log.Debugf("ExtWorkspace: Workspace %d removed", workspaceID)
|
|
||||||
|
|
||||||
m.post(func() {
|
|
||||||
ws.removed = true
|
|
||||||
|
|
||||||
m.workspaces.Delete(workspaceID)
|
|
||||||
|
|
||||||
m.wlMutex.Lock()
|
|
||||||
handle.Destroy()
|
|
||||||
m.wlMutex.Unlock()
|
|
||||||
|
|
||||||
m.updateState()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) updateState() {
|
|
||||||
groups := make([]*WorkspaceGroup, 0)
|
|
||||||
|
|
||||||
m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
|
|
||||||
if group.removed {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
outputs := make([]string, 0)
|
|
||||||
for outputID := range group.outputIDs {
|
|
||||||
if name, ok := m.outputNames.Load(outputID); ok && name != "" {
|
|
||||||
outputs = append(outputs, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaces := make([]*Workspace, 0)
|
|
||||||
for _, wsID := range group.workspaceIDs {
|
|
||||||
ws, exists := m.workspaces.Load(wsID)
|
|
||||||
if !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ws.removed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace := &Workspace{
|
|
||||||
ID: ws.workspaceID,
|
|
||||||
Name: ws.name,
|
|
||||||
Coordinates: ws.coordinates,
|
|
||||||
State: ws.state,
|
|
||||||
Active: ws.state&uint32(ext_workspace.ExtWorkspaceHandleV1StateActive) != 0,
|
|
||||||
Urgent: ws.state&uint32(ext_workspace.ExtWorkspaceHandleV1StateUrgent) != 0,
|
|
||||||
Hidden: ws.state&uint32(ext_workspace.ExtWorkspaceHandleV1StateHidden) != 0,
|
|
||||||
}
|
|
||||||
workspaces = append(workspaces, workspace)
|
|
||||||
}
|
|
||||||
|
|
||||||
groupState := &WorkspaceGroup{
|
|
||||||
ID: fmt.Sprintf("group-%d", group.id),
|
|
||||||
Outputs: outputs,
|
|
||||||
Workspaces: workspaces,
|
|
||||||
}
|
|
||||||
groups = append(groups, groupState)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
newState := State{
|
|
||||||
Groups: groups,
|
|
||||||
}
|
|
||||||
|
|
||||||
m.stateMutex.Lock()
|
|
||||||
m.state = &newState
|
|
||||||
m.stateMutex.Unlock()
|
|
||||||
|
|
||||||
m.notifySubscribers()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) notifier() {
|
|
||||||
defer m.notifierWg.Done()
|
|
||||||
const minGap = 100 * time.Millisecond
|
|
||||||
timer := time.NewTimer(minGap)
|
|
||||||
timer.Stop()
|
|
||||||
var pending bool
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-m.stopChan:
|
|
||||||
timer.Stop()
|
|
||||||
return
|
|
||||||
case <-m.dirty:
|
|
||||||
if pending {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pending = true
|
|
||||||
timer.Reset(minGap)
|
|
||||||
case <-timer.C:
|
|
||||||
if !pending {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
currentState := m.GetState()
|
|
||||||
|
|
||||||
if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) {
|
|
||||||
pending = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
m.subscribers.Range(func(key string, ch chan State) bool {
|
|
||||||
select {
|
|
||||||
case ch <- currentState:
|
|
||||||
default:
|
|
||||||
log.Warn("ExtWorkspace: subscriber channel full, dropping update")
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
stateCopy := currentState
|
|
||||||
m.lastNotified = &stateCopy
|
|
||||||
pending = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) ActivateWorkspace(groupID, workspaceID string) error {
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
|
|
||||||
m.post(func() {
|
|
||||||
var targetGroupID uint32
|
|
||||||
if groupID != "" {
|
|
||||||
var parsedID uint32
|
|
||||||
if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil {
|
|
||||||
targetGroupID = parsedID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var found bool
|
|
||||||
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
|
|
||||||
if targetGroupID != 0 && ws.groupID != targetGroupID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if ws.workspaceID == workspaceID || ws.name == workspaceID {
|
|
||||||
m.wlMutex.Lock()
|
|
||||||
err := ws.handle.Activate()
|
|
||||||
if err == nil {
|
|
||||||
err = m.manager.Commit()
|
|
||||||
}
|
|
||||||
m.wlMutex.Unlock()
|
|
||||||
errChan <- err
|
|
||||||
found = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return <-errChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) DeactivateWorkspace(groupID, workspaceID string) error {
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
|
|
||||||
m.post(func() {
|
|
||||||
var targetGroupID uint32
|
|
||||||
if groupID != "" {
|
|
||||||
var parsedID uint32
|
|
||||||
if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil {
|
|
||||||
targetGroupID = parsedID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var found bool
|
|
||||||
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
|
|
||||||
if targetGroupID != 0 && ws.groupID != targetGroupID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if ws.workspaceID == workspaceID || ws.name == workspaceID {
|
|
||||||
m.wlMutex.Lock()
|
|
||||||
err := ws.handle.Deactivate()
|
|
||||||
if err == nil {
|
|
||||||
err = m.manager.Commit()
|
|
||||||
}
|
|
||||||
m.wlMutex.Unlock()
|
|
||||||
errChan <- err
|
|
||||||
found = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return <-errChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) RemoveWorkspace(groupID, workspaceID string) error {
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
|
|
||||||
m.post(func() {
|
|
||||||
var targetGroupID uint32
|
|
||||||
if groupID != "" {
|
|
||||||
var parsedID uint32
|
|
||||||
if _, err := fmt.Sscanf(groupID, "group-%d", &parsedID); err == nil {
|
|
||||||
targetGroupID = parsedID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var found bool
|
|
||||||
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
|
|
||||||
if targetGroupID != 0 && ws.groupID != targetGroupID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if ws.workspaceID == workspaceID || ws.name == workspaceID {
|
|
||||||
m.wlMutex.Lock()
|
|
||||||
err := ws.handle.Remove()
|
|
||||||
if err == nil {
|
|
||||||
err = m.manager.Commit()
|
|
||||||
}
|
|
||||||
m.wlMutex.Unlock()
|
|
||||||
errChan <- err
|
|
||||||
found = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
errChan <- fmt.Errorf("workspace not found: %s in group %s", workspaceID, groupID)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return <-errChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) CreateWorkspace(groupID, workspaceName string) error {
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
|
|
||||||
m.post(func() {
|
|
||||||
var found bool
|
|
||||||
m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
|
|
||||||
if fmt.Sprintf("group-%d", group.id) == groupID {
|
|
||||||
m.wlMutex.Lock()
|
|
||||||
err := group.handle.CreateWorkspace(workspaceName)
|
|
||||||
if err == nil {
|
|
||||||
err = m.manager.Commit()
|
|
||||||
}
|
|
||||||
m.wlMutex.Unlock()
|
|
||||||
errChan <- err
|
|
||||||
found = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
errChan <- fmt.Errorf("workspace group not found: %s", groupID)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return <-errChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Close() {
|
|
||||||
close(m.stopChan)
|
|
||||||
m.wg.Wait()
|
|
||||||
m.notifierWg.Wait()
|
|
||||||
|
|
||||||
m.subscribers.Range(func(key string, ch chan State) bool {
|
|
||||||
close(ch)
|
|
||||||
m.subscribers.Delete(key)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
m.workspaces.Range(func(key uint32, ws *workspaceState) bool {
|
|
||||||
if ws.handle != nil {
|
|
||||||
ws.handle.Destroy()
|
|
||||||
}
|
|
||||||
m.workspaces.Delete(key)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
m.groups.Range(func(key uint32, group *workspaceGroupState) bool {
|
|
||||||
if group.handle != nil {
|
|
||||||
group.handle.Destroy()
|
|
||||||
}
|
|
||||||
m.groups.Delete(key)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if m.manager != nil {
|
|
||||||
m.manager.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
package extworkspace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStateChanged_BothNil(t *testing.T) {
|
|
||||||
assert.True(t, stateChanged(nil, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OneNil(t *testing.T) {
|
|
||||||
s := &State{Groups: []*WorkspaceGroup{}}
|
|
||||||
assert.True(t, stateChanged(s, nil))
|
|
||||||
assert.True(t, stateChanged(nil, s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_GroupCountDiffers(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1"}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_GroupIDDiffers(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{}, Workspaces: []*Workspace{}}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{ID: "group-2", Outputs: []string{}, Workspaces: []*Workspace{}}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputCountDiffers(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}, Workspaces: []*Workspace{}}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{}, Workspaces: []*Workspace{}}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_OutputNameDiffers(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}, Workspaces: []*Workspace{}}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"HDMI-A-1"}, Workspaces: []*Workspace{}}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_WorkspaceCountDiffers(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{{ID: "1", Name: "ws1"}},
|
|
||||||
}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{},
|
|
||||||
}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_WorkspaceFieldsDiffer(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{{
|
|
||||||
ID: "1", Name: "ws1", State: 0, Active: false, Urgent: false, Hidden: false,
|
|
||||||
}},
|
|
||||||
}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{{
|
|
||||||
ID: "2", Name: "ws1", State: 0, Active: false, Urgent: false, Hidden: false,
|
|
||||||
}},
|
|
||||||
}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].ID = "1"
|
|
||||||
b.Groups[0].Workspaces[0].Name = "ws2"
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].Name = "ws1"
|
|
||||||
b.Groups[0].Workspaces[0].State = 1
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].State = 0
|
|
||||||
b.Groups[0].Workspaces[0].Active = true
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].Active = false
|
|
||||||
b.Groups[0].Workspaces[0].Urgent = true
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].Urgent = false
|
|
||||||
b.Groups[0].Workspaces[0].Hidden = true
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_WorkspaceCoordinatesDiffer(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{{
|
|
||||||
ID: "1", Name: "ws1", Coordinates: []uint32{0, 0},
|
|
||||||
}},
|
|
||||||
}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{},
|
|
||||||
Workspaces: []*Workspace{{
|
|
||||||
ID: "1", Name: "ws1", Coordinates: []uint32{1, 0},
|
|
||||||
}},
|
|
||||||
}}}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
|
|
||||||
b.Groups[0].Workspaces[0].Coordinates = []uint32{0}
|
|
||||||
assert.True(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateChanged_Equal(t *testing.T) {
|
|
||||||
a := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{"eDP-1", "HDMI-A-1"},
|
|
||||||
Workspaces: []*Workspace{
|
|
||||||
{ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, State: 1, Active: true},
|
|
||||||
{ID: "2", Name: "ws2", Coordinates: []uint32{1, 0}, State: 0, Active: false},
|
|
||||||
},
|
|
||||||
}}}
|
|
||||||
b := &State{Groups: []*WorkspaceGroup{{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{"eDP-1", "HDMI-A-1"},
|
|
||||||
Workspaces: []*Workspace{
|
|
||||||
{ID: "1", Name: "ws1", Coordinates: []uint32{0, 0}, State: 1, Active: true},
|
|
||||||
{ID: "2", Name: "ws2", Coordinates: []uint32{1, 0}, State: 0, Active: false},
|
|
||||||
},
|
|
||||||
}}}
|
|
||||||
assert.False(t, stateChanged(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentGetState(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
state: &State{
|
|
||||||
Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 50
|
|
||||||
const iterations = 100
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
s := m.GetState()
|
|
||||||
_ = s.Groups
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.stateMutex.Lock()
|
|
||||||
m.state = &State{
|
|
||||||
Groups: []*WorkspaceGroup{{ID: "group-1", Outputs: []string{"eDP-1"}}},
|
|
||||||
}
|
|
||||||
m.stateMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
subID := string(rune('a' + id))
|
|
||||||
ch := m.Subscribe(subID)
|
|
||||||
assert.NotNil(t, ch)
|
|
||||||
time.Sleep(time.Millisecond)
|
|
||||||
m.Unsubscribe(subID)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_SyncmapGroupsConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
state := &workspaceGroupState{
|
|
||||||
id: key,
|
|
||||||
outputIDs: map[uint32]bool{1: true},
|
|
||||||
workspaceIDs: []uint32{uint32(j)},
|
|
||||||
}
|
|
||||||
m.groups.Store(key, state)
|
|
||||||
|
|
||||||
if loaded, ok := m.groups.Load(key); ok {
|
|
||||||
assert.Equal(t, key, loaded.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.groups.Range(func(k uint32, v *workspaceGroupState) bool {
|
|
||||||
_ = v.id
|
|
||||||
_ = v.outputIDs
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.groups.Delete(key)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_SyncmapWorkspacesConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
state := &workspaceState{
|
|
||||||
id: key,
|
|
||||||
workspaceID: "ws-1",
|
|
||||||
name: "workspace",
|
|
||||||
state: uint32(j % 4),
|
|
||||||
coordinates: []uint32{uint32(j), 0},
|
|
||||||
}
|
|
||||||
m.workspaces.Store(key, state)
|
|
||||||
|
|
||||||
if loaded, ok := m.workspaces.Load(key); ok {
|
|
||||||
assert.Equal(t, key, loaded.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.workspaces.Range(func(k uint32, v *workspaceState) bool {
|
|
||||||
_ = v.name
|
|
||||||
_ = v.state
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.workspaces.Delete(key)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_SyncmapOutputNamesConcurrentAccess(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 30
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
key := uint32(id)
|
|
||||||
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.outputNames.Store(key, "eDP-1")
|
|
||||||
|
|
||||||
if loaded, ok := m.outputNames.Load(key); ok {
|
|
||||||
assert.NotEmpty(t, loaded)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.outputNames.Range(func(k uint32, v string) bool {
|
|
||||||
_ = v
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.outputNames.Delete(key)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_NotifySubscribersNonBlocking(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
dirty: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
m.notifySubscribers()
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Len(t, m.dirty, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_PostQueueFull(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
cmdq: make(chan cmd, 2),
|
|
||||||
stopChan: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
m.post(func() {})
|
|
||||||
|
|
||||||
assert.Len(t, m.cmdq, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_GetStateNilState(t *testing.T) {
|
|
||||||
m := &Manager{}
|
|
||||||
|
|
||||||
s := m.GetState()
|
|
||||||
assert.NotNil(t, s.Groups)
|
|
||||||
assert.Empty(t, s.Groups)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWorkspace_Fields(t *testing.T) {
|
|
||||||
ws := Workspace{
|
|
||||||
ID: "ws-1",
|
|
||||||
Name: "workspace 1",
|
|
||||||
Coordinates: []uint32{0, 0},
|
|
||||||
State: 1,
|
|
||||||
Active: true,
|
|
||||||
Urgent: false,
|
|
||||||
Hidden: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "ws-1", ws.ID)
|
|
||||||
assert.Equal(t, "workspace 1", ws.Name)
|
|
||||||
assert.True(t, ws.Active)
|
|
||||||
assert.False(t, ws.Urgent)
|
|
||||||
assert.False(t, ws.Hidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWorkspaceGroup_Fields(t *testing.T) {
|
|
||||||
group := WorkspaceGroup{
|
|
||||||
ID: "group-1",
|
|
||||||
Outputs: []string{"eDP-1", "HDMI-A-1"},
|
|
||||||
Workspaces: []*Workspace{
|
|
||||||
{ID: "ws-1", Name: "workspace 1"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "group-1", group.ID)
|
|
||||||
assert.Len(t, group.Outputs, 2)
|
|
||||||
assert.Len(t, group.Workspaces, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewManager_GetRegistryError(t *testing.T) {
|
|
||||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
|
||||||
|
|
||||||
mockDisplay.EXPECT().Context().Return(nil)
|
|
||||||
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
|
||||||
|
|
||||||
_, err := NewManager(mockDisplay)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "failed to get registry")
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
package extworkspace
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_workspace"
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Workspace struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Coordinates []uint32 `json:"coordinates"`
|
|
||||||
State uint32 `json:"state"`
|
|
||||||
Active bool `json:"active"`
|
|
||||||
Urgent bool `json:"urgent"`
|
|
||||||
Hidden bool `json:"hidden"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkspaceGroup struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Outputs []string `json:"outputs"`
|
|
||||||
Workspaces []*Workspace `json:"workspaces"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type State struct {
|
|
||||||
Groups []*WorkspaceGroup `json:"groups"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type cmd struct {
|
|
||||||
fn func()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
display wlclient.WaylandDisplay
|
|
||||||
ctx *wlclient.Context
|
|
||||||
registry *wlclient.Registry
|
|
||||||
manager *ext_workspace.ExtWorkspaceManagerV1
|
|
||||||
|
|
||||||
outputNames syncmap.Map[uint32, string]
|
|
||||||
|
|
||||||
groups syncmap.Map[uint32, *workspaceGroupState]
|
|
||||||
|
|
||||||
workspaces syncmap.Map[uint32, *workspaceState]
|
|
||||||
|
|
||||||
wlMutex sync.Mutex
|
|
||||||
cmdq chan cmd
|
|
||||||
stopChan chan struct{}
|
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
subscribers syncmap.Map[string, chan State]
|
|
||||||
dirty chan struct{}
|
|
||||||
notifierWg sync.WaitGroup
|
|
||||||
lastNotified *State
|
|
||||||
|
|
||||||
stateMutex sync.RWMutex
|
|
||||||
state *State
|
|
||||||
}
|
|
||||||
|
|
||||||
type workspaceGroupState struct {
|
|
||||||
id uint32
|
|
||||||
handle *ext_workspace.ExtWorkspaceGroupHandleV1
|
|
||||||
outputIDs map[uint32]bool
|
|
||||||
workspaceIDs []uint32
|
|
||||||
removed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type workspaceState struct {
|
|
||||||
id uint32
|
|
||||||
handle *ext_workspace.ExtWorkspaceHandleV1
|
|
||||||
workspaceID string
|
|
||||||
name string
|
|
||||||
coordinates []uint32
|
|
||||||
state uint32
|
|
||||||
groupID uint32
|
|
||||||
removed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) GetState() State {
|
|
||||||
m.stateMutex.RLock()
|
|
||||||
defer m.stateMutex.RUnlock()
|
|
||||||
if m.state == nil {
|
|
||||||
return State{
|
|
||||||
Groups: []*WorkspaceGroup{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stateCopy := *m.state
|
|
||||||
return stateCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Subscribe(id string) chan State {
|
|
||||||
ch := make(chan State, 64)
|
|
||||||
|
|
||||||
m.subscribers.Store(id, ch)
|
|
||||||
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Unsubscribe(id string) {
|
|
||||||
if ch, ok := m.subscribers.LoadAndDelete(id); ok {
|
|
||||||
close(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) notifySubscribers() {
|
|
||||||
select {
|
|
||||||
case m.dirty <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stateChanged(old, new *State) bool {
|
|
||||||
if old == nil || new == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if len(old.Groups) != len(new.Groups) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, newGroup := range new.Groups {
|
|
||||||
if i >= len(old.Groups) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
oldGroup := old.Groups[i]
|
|
||||||
if oldGroup.ID != newGroup.ID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if len(oldGroup.Outputs) != len(newGroup.Outputs) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for j, newOutput := range newGroup.Outputs {
|
|
||||||
if j >= len(oldGroup.Outputs) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if oldGroup.Outputs[j] != newOutput {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(oldGroup.Workspaces) != len(newGroup.Workspaces) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for j, newWs := range newGroup.Workspaces {
|
|
||||||
if j >= len(oldGroup.Workspaces) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
oldWs := oldGroup.Workspaces[j]
|
|
||||||
if oldWs.ID != newWs.ID || oldWs.Name != newWs.Name || oldWs.State != newWs.State {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if oldWs.Active != newWs.Active || oldWs.Urgent != newWs.Urgent || oldWs.Hidden != newWs.Hidden {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if len(oldWs.Coordinates) != len(newWs.Coordinates) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for k, coord := range newWs.Coordinates {
|
|
||||||
if k >= len(oldWs.Coordinates) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if oldWs.Coordinates[k] != coord {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
|
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
@@ -138,27 +137,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(req.Method, "extworkspace.") {
|
|
||||||
if extWorkspaceManager == nil {
|
|
||||||
if extWorkspaceAvailable.Load() {
|
|
||||||
extWorkspaceInitMutex.Lock()
|
|
||||||
if extWorkspaceManager == nil {
|
|
||||||
if err := InitializeExtWorkspaceManager(); err != nil {
|
|
||||||
extWorkspaceInitMutex.Unlock()
|
|
||||||
models.RespondError(conn, req.ID, "extworkspace manager not available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
extWorkspaceInitMutex.Unlock()
|
|
||||||
} else {
|
|
||||||
models.RespondError(conn, req.ID, "extworkspace manager not initialized")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
extworkspace.HandleRequest(conn, req, extWorkspaceManager)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(req.Method, "wlroutput.") {
|
if strings.HasPrefix(req.Method, "wlroutput.") {
|
||||||
if wlrOutputManager == nil {
|
if wlrOutputManager == nil {
|
||||||
models.RespondError(conn, req.ID, "wlroutput manager not initialized")
|
models.RespondError(conn, req.ID, "wlroutput manager not initialized")
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import (
|
|||||||
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
|
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
@@ -68,7 +67,6 @@ var appPickerManager *apppicker.Manager
|
|||||||
var cupsManager *cups.Manager
|
var cupsManager *cups.Manager
|
||||||
var tailscaleManager *tailscale.Manager
|
var tailscaleManager *tailscale.Manager
|
||||||
var dwlManager *dwl.Manager
|
var dwlManager *dwl.Manager
|
||||||
var extWorkspaceManager *extworkspace.Manager
|
|
||||||
var brightnessManager *brightness.Manager
|
var brightnessManager *brightness.Manager
|
||||||
var wlrOutputManager *wlroutput.Manager
|
var wlrOutputManager *wlroutput.Manager
|
||||||
var evdevManager *evdev.Manager
|
var evdevManager *evdev.Manager
|
||||||
@@ -86,8 +84,6 @@ const dbusClientID = "dms-dbus-client"
|
|||||||
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
||||||
var cupsSubscribers syncmap.Map[string, bool]
|
var cupsSubscribers syncmap.Map[string, bool]
|
||||||
var cupsSubscriberCount atomic.Int32
|
var cupsSubscriberCount atomic.Int32
|
||||||
var extWorkspaceAvailable atomic.Bool
|
|
||||||
var extWorkspaceInitMutex sync.Mutex
|
|
||||||
|
|
||||||
func getSocketDir() string {
|
func getSocketDir() string {
|
||||||
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
|
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
|
||||||
@@ -293,30 +289,6 @@ func InitializeBrightnessManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeExtWorkspaceManager() error {
|
|
||||||
log.Info("Attempting to initialize ExtWorkspace...")
|
|
||||||
|
|
||||||
if wlContext == nil {
|
|
||||||
ctx, err := wlcontext.New()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to create shared Wayland context: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
wlContext = ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
manager, err := extworkspace.NewManager(wlContext.Display())
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("Failed to initialize extworkspace manager: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
extWorkspaceManager = manager
|
|
||||||
|
|
||||||
log.Info("ExtWorkspace initialized successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitializeWlrOutputManager() error {
|
func InitializeWlrOutputManager() error {
|
||||||
log.Info("Attempting to initialize WlrOutput management...")
|
log.Info("Attempting to initialize WlrOutput management...")
|
||||||
|
|
||||||
@@ -499,10 +471,6 @@ func getCapabilities() Capabilities {
|
|||||||
caps = append(caps, "dwl")
|
caps = append(caps, "dwl")
|
||||||
}
|
}
|
||||||
|
|
||||||
if extWorkspaceAvailable.Load() {
|
|
||||||
caps = append(caps, "extworkspace")
|
|
||||||
}
|
|
||||||
|
|
||||||
if brightnessManager != nil {
|
if brightnessManager != nil {
|
||||||
caps = append(caps, "brightness")
|
caps = append(caps, "brightness")
|
||||||
}
|
}
|
||||||
@@ -573,10 +541,6 @@ func getServerInfo() ServerInfo {
|
|||||||
caps = append(caps, "dwl")
|
caps = append(caps, "dwl")
|
||||||
}
|
}
|
||||||
|
|
||||||
if extWorkspaceAvailable.Load() {
|
|
||||||
caps = append(caps, "extworkspace")
|
|
||||||
}
|
|
||||||
|
|
||||||
if brightnessManager != nil {
|
if brightnessManager != nil {
|
||||||
caps = append(caps, "brightness")
|
caps = append(caps, "brightness")
|
||||||
}
|
}
|
||||||
@@ -1113,50 +1077,6 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldSubscribe("extworkspace") {
|
|
||||||
if extWorkspaceManager == nil && extWorkspaceAvailable.Load() {
|
|
||||||
extWorkspaceInitMutex.Lock()
|
|
||||||
if extWorkspaceManager == nil {
|
|
||||||
if err := InitializeExtWorkspaceManager(); err != nil {
|
|
||||||
log.Warnf("Failed to initialize ExtWorkspace manager for subscription: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
extWorkspaceInitMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if extWorkspaceManager != nil {
|
|
||||||
wg.Add(1)
|
|
||||||
extWorkspaceChan := extWorkspaceManager.Subscribe(clientID + "-extworkspace")
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
defer extWorkspaceManager.Unsubscribe(clientID + "-extworkspace")
|
|
||||||
|
|
||||||
initialState := extWorkspaceManager.GetState()
|
|
||||||
select {
|
|
||||||
case eventChan <- ServiceEvent{Service: "extworkspace", Data: initialState}:
|
|
||||||
case <-stopChan:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case state, ok := <-extWorkspaceChan:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case eventChan <- ServiceEvent{Service: "extworkspace", Data: state}:
|
|
||||||
case <-stopChan:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-stopChan:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if shouldSubscribe("brightness") && brightnessManager != nil {
|
if shouldSubscribe("brightness") && brightnessManager != nil {
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
brightnessStateChan := brightnessManager.Subscribe(clientID + "-brightness-state")
|
brightnessStateChan := brightnessManager.Subscribe(clientID + "-brightness-state")
|
||||||
@@ -1415,9 +1335,6 @@ func cleanupManagers() {
|
|||||||
if dwlManager != nil {
|
if dwlManager != nil {
|
||||||
dwlManager.Close()
|
dwlManager.Close()
|
||||||
}
|
}
|
||||||
if extWorkspaceManager != nil {
|
|
||||||
extWorkspaceManager.Close()
|
|
||||||
}
|
|
||||||
if brightnessManager != nil {
|
if brightnessManager != nil {
|
||||||
brightnessManager.Close()
|
brightnessManager.Close()
|
||||||
}
|
}
|
||||||
@@ -1597,13 +1514,6 @@ func Start(printDocs bool) error {
|
|||||||
log.Info(" - appId : Focused window app ID")
|
log.Info(" - appId : Focused window app ID")
|
||||||
log.Info(" - kbLayout : Current keyboard layout")
|
log.Info(" - kbLayout : Current keyboard layout")
|
||||||
log.Info(" - keymode : Current keybind mode")
|
log.Info(" - keymode : Current keybind mode")
|
||||||
log.Info("ExtWorkspace:")
|
|
||||||
log.Info(" extworkspace.getState - Get current workspace state (groups, workspaces)")
|
|
||||||
log.Info(" extworkspace.activateWorkspace - Activate workspace (params: groupID, workspaceID)")
|
|
||||||
log.Info(" extworkspace.deactivateWorkspace - Deactivate workspace (params: groupID, workspaceID)")
|
|
||||||
log.Info(" extworkspace.removeWorkspace - Remove workspace (params: groupID, workspaceID)")
|
|
||||||
log.Info(" extworkspace.createWorkspace - Create workspace (params: groupID, name)")
|
|
||||||
log.Info(" extworkspace.subscribe - Subscribe to workspace state changes (streaming)")
|
|
||||||
log.Info("Brightness:")
|
log.Info("Brightness:")
|
||||||
log.Info(" brightness.getState - Get current brightness state for all devices")
|
log.Info(" brightness.getState - Get current brightness state for all devices")
|
||||||
log.Info(" brightness.setBrightness - Set device brightness (params: device, percent)")
|
log.Info(" brightness.setBrightness - Set device brightness (params: device, percent)")
|
||||||
@@ -1784,14 +1694,6 @@ func Start(printDocs bool) error {
|
|||||||
log.Debugf("DWL manager unavailable: %v", err)
|
log.Debugf("DWL manager unavailable: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if extworkspace.CheckCapability() {
|
|
||||||
extWorkspaceAvailable.Store(true)
|
|
||||||
log.Info("ExtWorkspace capability detected and will be available on subscription")
|
|
||||||
} else {
|
|
||||||
log.Debug("ExtWorkspace capability not available")
|
|
||||||
extWorkspaceAvailable.Store(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := InitializeWlrOutputManager(); err != nil {
|
if err := InitializeWlrOutputManager(); err != nil {
|
||||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
log.Debugf("WlrOutput manager unavailable: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Quickshell
|
|||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import Quickshell.I3
|
import Quickshell.I3
|
||||||
|
import Quickshell.WindowManager
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -86,7 +87,22 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && !CompositorService.isMiracle && ExtWorkspaceService.extWorkspaceAvailable)
|
readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(parentScreen) : null
|
||||||
|
readonly property bool useExtWorkspace: {
|
||||||
|
if (Quickshell.env("DMS_FORCE_EXTWS") === "1")
|
||||||
|
return (WindowManager.windowsets?.length ?? 0) > 0;
|
||||||
|
switch (CompositorService.compositor) {
|
||||||
|
case "niri":
|
||||||
|
case "hyprland":
|
||||||
|
case "dwl":
|
||||||
|
case "sway":
|
||||||
|
case "scroll":
|
||||||
|
case "miracle":
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return (WindowManager.windowsets?.length ?? 0) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: DesktopEntries
|
target: DesktopEntries
|
||||||
@@ -361,7 +377,7 @@ Item {
|
|||||||
"id": "",
|
"id": "",
|
||||||
"name": "",
|
"name": "",
|
||||||
"active": false,
|
"active": false,
|
||||||
"hidden": true
|
"_placeholder": true
|
||||||
};
|
};
|
||||||
} else if (CompositorService.isNiri) {
|
} else if (CompositorService.isNiri) {
|
||||||
placeholder = {
|
placeholder = {
|
||||||
@@ -493,33 +509,21 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getExtWorkspaceWorkspaces() {
|
function getExtWorkspaceWorkspaces() {
|
||||||
const groups = ExtWorkspaceService.groups;
|
const fallback = [
|
||||||
if (!ExtWorkspaceService.extWorkspaceAvailable || groups.length === 0) {
|
{
|
||||||
return [
|
"id": "1",
|
||||||
{
|
"name": "1",
|
||||||
"id": "1",
|
"active": false
|
||||||
"name": "1",
|
}
|
||||||
"active": false
|
];
|
||||||
}
|
if (!extProjection)
|
||||||
];
|
return fallback;
|
||||||
}
|
|
||||||
|
|
||||||
const group = groups.find(g => g.outputs && g.outputs.includes(root.screenName));
|
let visible = extProjection.windowsets.filter(ws => ws.shouldDisplay);
|
||||||
if (!group || !group.workspaces) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "1",
|
|
||||||
"active": false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
let visible = group.workspaces.filter(ws => !ws.hidden);
|
|
||||||
|
|
||||||
const hasValidCoordinates = visible.some(ws => ws.coordinates && ws.coordinates.length > 0);
|
const hasValidCoordinates = visible.some(ws => ws.coordinates && ws.coordinates.length > 0);
|
||||||
if (hasValidCoordinates) {
|
if (hasValidCoordinates) {
|
||||||
visible = visible.sort((a, b) => {
|
visible = visible.slice().sort((a, b) => {
|
||||||
const coordsA = a.coordinates || [0, 0];
|
const coordsA = a.coordinates || [0, 0];
|
||||||
const coordsB = b.coordinates || [0, 0];
|
const coordsB = b.coordinates || [0, 0];
|
||||||
if (coordsA[0] !== coordsB[0])
|
if (coordsA[0] !== coordsB[0])
|
||||||
@@ -528,33 +532,14 @@ Item {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
visible = visible.map(ws => ({
|
return visible.length > 0 ? visible : fallback;
|
||||||
id: ws.id,
|
|
||||||
name: ws.name,
|
|
||||||
coordinates: ws.coordinates,
|
|
||||||
state: ws.state,
|
|
||||||
active: ws.active,
|
|
||||||
urgent: ws.urgent,
|
|
||||||
hidden: ws.hidden,
|
|
||||||
groupID: group.id
|
|
||||||
}));
|
|
||||||
|
|
||||||
return visible.length > 0 ? visible : [
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "1",
|
|
||||||
"active": false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExtWorkspaceActiveWorkspace() {
|
function getExtWorkspaceActiveWorkspace() {
|
||||||
if (!ExtWorkspaceService.extWorkspaceAvailable) {
|
if (!extProjection)
|
||||||
return 1;
|
return "";
|
||||||
}
|
const activeWs = extProjection.windowsets.find(ws => ws.active);
|
||||||
|
return activeWs ? (activeWs.id || activeWs.name || "") : "";
|
||||||
const activeWs = ExtWorkspaceService.getActiveWorkspaceForOutput(root.screenName);
|
|
||||||
return activeWs ? (activeWs.id || activeWs.name || "1") : "1";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property real dpr: parentScreen ? CompositorService.getScreenScale(parentScreen) : 1
|
readonly property real dpr: parentScreen ? CompositorService.getScreenScale(parentScreen) : 1
|
||||||
@@ -566,7 +551,7 @@ Item {
|
|||||||
function getRealWorkspaces() {
|
function getRealWorkspaces() {
|
||||||
return root.workspaceList.filter(ws => {
|
return root.workspaceList.filter(ws => {
|
||||||
if (useExtWorkspace)
|
if (useExtWorkspace)
|
||||||
return ws && (ws.id !== "" || ws.name !== "") && !ws.hidden;
|
return ws && !ws._placeholder;
|
||||||
if (CompositorService.isNiri)
|
if (CompositorService.isNiri)
|
||||||
return ws && ws.idx !== -1;
|
return ws && ws.idx !== -1;
|
||||||
if (CompositorService.isHyprland)
|
if (CompositorService.isHyprland)
|
||||||
@@ -583,8 +568,9 @@ Item {
|
|||||||
if (!data)
|
if (!data)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (root.useExtWorkspace && (data.id || data.name)) {
|
if (root.useExtWorkspace) {
|
||||||
ExtWorkspaceService.activateWorkspace(data.id || data.name, data.groupID || "");
|
if (typeof data.activate === "function")
|
||||||
|
data.activate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,7 +635,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextWorkspace = realWorkspaces[nextIndex];
|
const nextWorkspace = realWorkspaces[nextIndex];
|
||||||
ExtWorkspaceService.activateWorkspace(nextWorkspace.id || nextWorkspace.name, nextWorkspace.groupID || "");
|
if (typeof nextWorkspace.activate === "function")
|
||||||
|
nextWorkspace.activate();
|
||||||
} else if (CompositorService.isNiri) {
|
} else if (CompositorService.isNiri) {
|
||||||
const realWorkspaces = getRealWorkspaces();
|
const realWorkspaces = getRealWorkspaces();
|
||||||
if (realWorkspaces.length < 2) {
|
if (realWorkspaces.length < 2) {
|
||||||
@@ -1013,7 +1000,7 @@ Item {
|
|||||||
}
|
}
|
||||||
property bool isPlaceholder: {
|
property bool isPlaceholder: {
|
||||||
if (root.useExtWorkspace)
|
if (root.useExtWorkspace)
|
||||||
return !!(modelData && modelData.hidden);
|
return !!(modelData && modelData._placeholder);
|
||||||
if (CompositorService.isNiri)
|
if (CompositorService.isNiri)
|
||||||
return !!(modelData && modelData.idx === -1);
|
return !!(modelData && modelData.idx === -1);
|
||||||
if (CompositorService.isHyprland)
|
if (CompositorService.isHyprland)
|
||||||
@@ -1313,8 +1300,9 @@ Item {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
if (mouse.button === Qt.LeftButton) {
|
||||||
if (root.useExtWorkspace && (modelData?.id || modelData?.name)) {
|
if (root.useExtWorkspace) {
|
||||||
ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || "");
|
if (typeof modelData?.activate === "function")
|
||||||
|
modelData.activate();
|
||||||
} else if (CompositorService.isNiri) {
|
} else if (CompositorService.isNiri) {
|
||||||
if (modelData && modelData.id !== undefined) {
|
if (modelData && modelData.id !== undefined) {
|
||||||
NiriService.switchToWorkspace(modelData.id);
|
NiriService.switchToWorkspace(modelData.id);
|
||||||
@@ -1941,9 +1929,9 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: ExtWorkspaceService
|
target: WindowManager
|
||||||
enabled: root.useExtWorkspace
|
enabled: root.useExtWorkspace
|
||||||
function onStateChanged() {
|
function onWindowsetsChanged() {
|
||||||
delegateRoot.updateAllData();
|
delegateRoot.updateAllData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1952,9 +1940,6 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
|
|
||||||
DMSService.addSubscription("extworkspace");
|
|
||||||
}
|
|
||||||
_updateBlurRegistration();
|
_updateBlurRegistration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ Singleton {
|
|||||||
property bool isConnected: false
|
property bool isConnected: false
|
||||||
property bool isConnecting: false
|
property bool isConnecting: false
|
||||||
property bool subscribeConnected: false
|
property bool subscribeConnected: false
|
||||||
readonly property bool forceExtWorkspace: false
|
|
||||||
|
|
||||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
||||||
|
|
||||||
@@ -53,7 +52,6 @@ Singleton {
|
|||||||
signal dwlStateUpdate(var data)
|
signal dwlStateUpdate(var data)
|
||||||
signal brightnessStateUpdate(var data)
|
signal brightnessStateUpdate(var data)
|
||||||
signal brightnessDeviceUpdate(var device)
|
signal brightnessDeviceUpdate(var device)
|
||||||
signal extWorkspaceStateUpdate(var data)
|
|
||||||
signal wlrOutputStateUpdate(var data)
|
signal wlrOutputStateUpdate(var data)
|
||||||
signal evdevStateUpdate(var data)
|
signal evdevStateUpdate(var data)
|
||||||
signal gammaStateUpdate(var data)
|
signal gammaStateUpdate(var data)
|
||||||
@@ -288,7 +286,7 @@ Singleton {
|
|||||||
|
|
||||||
function removeSubscription(service) {
|
function removeSubscription(service) {
|
||||||
if (activeSubscriptions.includes("all")) {
|
if (activeSubscriptions.includes("all")) {
|
||||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace", "browser", "location"];
|
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "browser", "location"];
|
||||||
const filtered = allServices.filter(s => s !== service);
|
const filtered = allServices.filter(s => s !== service);
|
||||||
subscribe(filtered);
|
subscribe(filtered);
|
||||||
} else {
|
} else {
|
||||||
@@ -310,7 +308,7 @@ Singleton {
|
|||||||
excludeServices = [excludeServices];
|
excludeServices = [excludeServices];
|
||||||
}
|
}
|
||||||
|
|
||||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus", "location"];
|
const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "dwl", "brightness", "browser", "dbus", "location"];
|
||||||
const filtered = allServices.filter(s => !excludeServices.includes(s));
|
const filtered = allServices.filter(s => !excludeServices.includes(s));
|
||||||
subscribe(filtered);
|
subscribe(filtered);
|
||||||
}
|
}
|
||||||
@@ -364,8 +362,6 @@ Singleton {
|
|||||||
if (data.device) {
|
if (data.device) {
|
||||||
brightnessDeviceUpdate(data.device);
|
brightnessDeviceUpdate(data.device);
|
||||||
}
|
}
|
||||||
} else if (service === "extworkspace") {
|
|
||||||
extWorkspaceStateUpdate(data);
|
|
||||||
} else if (service === "wlroutput") {
|
} else if (service === "wlroutput") {
|
||||||
wlrOutputStateUpdate(data);
|
wlrOutputStateUpdate(data);
|
||||||
} else if (service === "evdev") {
|
} else if (service === "evdev") {
|
||||||
@@ -752,12 +748,6 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renameWorkspace(name, callback) {
|
|
||||||
sendRequest("extworkspace.renameWorkspace", {
|
|
||||||
"name": name
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sysupdateGetState(callback) {
|
function sysupdateGetState(callback) {
|
||||||
sendRequest("sysupdate.getState", null, callback);
|
sendRequest("sysupdate.getState", null, callback);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,279 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
readonly property var log: Log.scoped("ExtWorkspaceService")
|
|
||||||
|
|
||||||
property bool extWorkspaceAvailable: false
|
|
||||||
property var groups: []
|
|
||||||
property var _cachedWorkspaces: ({})
|
|
||||||
|
|
||||||
signal stateChanged
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DMSService
|
|
||||||
function onCapabilitiesReceived() {
|
|
||||||
checkCapabilities();
|
|
||||||
}
|
|
||||||
function onConnectionStateChanged() {
|
|
||||||
if (DMSService.isConnected) {
|
|
||||||
checkCapabilities();
|
|
||||||
} else {
|
|
||||||
extWorkspaceAvailable = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onExtWorkspaceStateUpdate(data) {
|
|
||||||
if (extWorkspaceAvailable) {
|
|
||||||
handleStateUpdate(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (DMSService.dmsAvailable) {
|
|
||||||
checkCapabilities();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkCapabilities() {
|
|
||||||
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
|
|
||||||
extWorkspaceAvailable = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasExtWorkspace = DMSService.capabilities.includes("extworkspace");
|
|
||||||
if (hasExtWorkspace && !extWorkspaceAvailable) {
|
|
||||||
if (typeof CompositorService !== "undefined") {
|
|
||||||
const useExtWorkspace = DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && !CompositorService.isMiracle);
|
|
||||||
if (!useExtWorkspace) {
|
|
||||||
log.info("ext-workspace available but compositor has native support");
|
|
||||||
extWorkspaceAvailable = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
extWorkspaceAvailable = true;
|
|
||||||
log.info("ext-workspace capability detected");
|
|
||||||
DMSService.addSubscription("extworkspace");
|
|
||||||
requestState();
|
|
||||||
} else if (!hasExtWorkspace) {
|
|
||||||
extWorkspaceAvailable = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestState() {
|
|
||||||
if (!DMSService.isConnected || !extWorkspaceAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DMSService.sendRequest("extworkspace.getState", null, response => {
|
|
||||||
if (response.result) {
|
|
||||||
handleStateUpdate(response.result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStateUpdate(state) {
|
|
||||||
groups = state.groups || [];
|
|
||||||
if (groups.length === 0) {
|
|
||||||
log.warn("Received empty workspace groups from backend");
|
|
||||||
} else {
|
|
||||||
log.debug("Updated with", groups.length, "workspace groups");
|
|
||||||
}
|
|
||||||
stateChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
function activateWorkspace(workspaceID, groupID = "") {
|
|
||||||
if (!DMSService.isConnected || !extWorkspaceAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DMSService.sendRequest("extworkspace.activateWorkspace", {
|
|
||||||
"workspaceID": workspaceID,
|
|
||||||
"groupID": groupID
|
|
||||||
}, response => {
|
|
||||||
if (response.error) {
|
|
||||||
log.warn("activateWorkspace error:", response.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deactivateWorkspace(workspaceID, groupID = "") {
|
|
||||||
if (!DMSService.isConnected || !extWorkspaceAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DMSService.sendRequest("extworkspace.deactivateWorkspace", {
|
|
||||||
"workspaceID": workspaceID,
|
|
||||||
"groupID": groupID
|
|
||||||
}, response => {
|
|
||||||
if (response.error) {
|
|
||||||
log.warn("deactivateWorkspace error:", response.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeWorkspace(workspaceID, groupID = "") {
|
|
||||||
if (!DMSService.isConnected || !extWorkspaceAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DMSService.sendRequest("extworkspace.removeWorkspace", {
|
|
||||||
"workspaceID": workspaceID,
|
|
||||||
"groupID": groupID
|
|
||||||
}, response => {
|
|
||||||
if (response.error) {
|
|
||||||
log.warn("removeWorkspace error:", response.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWorkspace(groupID, name) {
|
|
||||||
if (!DMSService.isConnected || !extWorkspaceAvailable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DMSService.sendRequest("extworkspace.createWorkspace", {
|
|
||||||
"groupID": groupID,
|
|
||||||
"name": name
|
|
||||||
}, response => {
|
|
||||||
if (response.error) {
|
|
||||||
log.warn("createWorkspace error:", response.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGroupForOutput(outputName) {
|
|
||||||
for (const group of groups) {
|
|
||||||
if (group.outputs && group.outputs.includes(outputName)) {
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWorkspacesForOutput(outputName) {
|
|
||||||
const group = getGroupForOutput(outputName);
|
|
||||||
return group ? (group.workspaces || []) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveWorkspaces() {
|
|
||||||
const active = [];
|
|
||||||
for (const group of groups) {
|
|
||||||
if (!group.workspaces)
|
|
||||||
continue;
|
|
||||||
for (const ws of group.workspaces) {
|
|
||||||
if (ws.active) {
|
|
||||||
active.push({
|
|
||||||
workspace: ws,
|
|
||||||
group: group,
|
|
||||||
outputs: group.outputs || []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return active;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveWorkspaceForOutput(outputName) {
|
|
||||||
const group = getGroupForOutput(outputName);
|
|
||||||
if (!group || !group.workspaces)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
for (const ws of group.workspaces) {
|
|
||||||
if (ws.active) {
|
|
||||||
return ws;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVisibleWorkspaces(outputName) {
|
|
||||||
const workspaces = getWorkspacesForOutput(outputName);
|
|
||||||
let visible = workspaces.filter(ws => !ws.hidden);
|
|
||||||
|
|
||||||
const hasValidCoordinates = visible.some(ws => ws.coordinates && ws.coordinates.length > 0);
|
|
||||||
if (hasValidCoordinates) {
|
|
||||||
visible = visible.sort((a, b) => {
|
|
||||||
const coordsA = a.coordinates || [0, 0];
|
|
||||||
const coordsB = b.coordinates || [0, 0];
|
|
||||||
if (coordsA[0] !== coordsB[0])
|
|
||||||
return coordsA[0] - coordsB[0];
|
|
||||||
return coordsA[1] - coordsB[1];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheKey = outputName;
|
|
||||||
if (!_cachedWorkspaces[cacheKey]) {
|
|
||||||
_cachedWorkspaces[cacheKey] = {
|
|
||||||
workspaces: [],
|
|
||||||
lastNames: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const cache = _cachedWorkspaces[cacheKey];
|
|
||||||
const currentNames = visible.map(ws => ws.name || ws.id);
|
|
||||||
const namesChanged = JSON.stringify(cache.lastNames) !== JSON.stringify(currentNames);
|
|
||||||
|
|
||||||
if (namesChanged || cache.workspaces.length !== visible.length) {
|
|
||||||
cache.workspaces = visible.map(ws => ({
|
|
||||||
id: ws.id,
|
|
||||||
name: ws.name,
|
|
||||||
coordinates: ws.coordinates,
|
|
||||||
state: ws.state,
|
|
||||||
active: ws.active,
|
|
||||||
urgent: ws.urgent,
|
|
||||||
hidden: ws.hidden
|
|
||||||
}));
|
|
||||||
cache.lastNames = currentNames;
|
|
||||||
return cache.workspaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < visible.length; i++) {
|
|
||||||
const src = visible[i];
|
|
||||||
const dst = cache.workspaces[i];
|
|
||||||
dst.id = src.id;
|
|
||||||
dst.name = src.name;
|
|
||||||
dst.coordinates = src.coordinates;
|
|
||||||
dst.state = src.state;
|
|
||||||
dst.active = src.active;
|
|
||||||
dst.urgent = src.urgent;
|
|
||||||
dst.hidden = src.hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cache.workspaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUrgentWorkspaces() {
|
|
||||||
const urgent = [];
|
|
||||||
for (const group of groups) {
|
|
||||||
if (!group.workspaces)
|
|
||||||
continue;
|
|
||||||
for (const ws of group.workspaces) {
|
|
||||||
if (ws.urgent) {
|
|
||||||
urgent.push({
|
|
||||||
workspace: ws,
|
|
||||||
group: group,
|
|
||||||
outputs: group.outputs || []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return urgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchToWorkspace(outputName, workspaceName) {
|
|
||||||
const workspaces = getWorkspacesForOutput(outputName);
|
|
||||||
for (const ws of workspaces) {
|
|
||||||
if (ws.name === workspaceName || ws.id === workspaceName) {
|
|
||||||
activateWorkspace(ws.name || ws.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.warn("workspace not found:", workspaceName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user