Workspaces: Implement ext_workspace protocol

Even though it isn't faster than Hyprland IPC, it still is more flexible
and more future proof.

Closes https://github.com/scorpion-26/gBar/issues/8
This commit is contained in:
scorpion-26 2023-03-03 20:44:51 +01:00
parent 63faae741f
commit ad92b7c12a
11 changed files with 677 additions and 42 deletions

View file

@ -1,4 +1,4 @@
# gBar # gBar
My personal blazingly fast and efficient status bar + widgets, in case anyone finds a use for it. My personal blazingly fast and efficient status bar + widgets, in case anyone finds a use for it.
*gBar: **G**TK **Bar*** *gBar: **G**TK **Bar***
@ -71,7 +71,7 @@ gBar bluetooth [monitor]
## Features / Widgets ## Features / Widgets
Bar: Bar:
- Workspaces (Hyprland only) - Workspaces (Hyprland only. Technically works on all compositors implementing ext_workspace, though workspace control relies on Hyprland)
- Time - Time
- Bluetooth (BlueZ only) - Bluetooth (BlueZ only)
- Audio control - Audio control

View file

@ -24,6 +24,8 @@
]; ];
buildInputs = [ buildInputs = [
wayland wayland
wayland-protocols
wayland-scanner
bluez bluez
gtk3 gtk3
gtk-layer-shell gtk-layer-shell

View file

@ -1,14 +1,28 @@
project('gBar', project('gBar',
['cpp'], ['c', 'cpp'],
version: '0.0.1', version: '0.0.1',
license: 'MIT', license: 'MIT',
meson_version: '>=0.49.0', meson_version: '>=0.49.0',
default_options: ['c_std=c++17', default_options: ['cpp_std=c++17',
'warning_level=3', 'warning_level=3',
'default_library=static', 'default_library=static',
'buildtype=release'], 'buildtype=release'],
) )
# Wayland protocols
wayland_client = dependency('wayland-client')
wayland_scanner = find_program('wayland-scanner')
ext_workspace_src = custom_target('generate-ext-workspace-src',
input: ['protocols/ext-workspace-unstable-v1.xml'],
output: ['ext-workspace-unstable-v1.c'],
command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'])
ext_workspace_header = custom_target('generate-ext-workspace-header',
input: ['protocols/ext-workspace-unstable-v1.xml'],
output: ['ext-workspace-unstable-v1.h'],
command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'])
gtk = dependency('gtk+-3.0') gtk = dependency('gtk+-3.0')
gtk_layer_shell = dependency('gtk-layer-shell-0') gtk_layer_shell = dependency('gtk-layer-shell-0')
@ -24,8 +38,12 @@ headers = [
] ]
if get_option('WithHyprland') if get_option('WithHyprland')
add_global_arguments('-DWITH_HYPRLAND', language: 'cpp') add_global_arguments('-DWITH_WORKSPACES', language: 'cpp')
headers += 'src/Hyprland.h' headers += 'src/Workspaces.h'
endif
if get_option('WithWorkspaces')
add_global_arguments('-DWITH_WORKSPACES', language: 'cpp')
headers += 'src/Workspaces.h'
endif endif
if get_option('WithNvidia') if get_option('WithNvidia')
add_global_arguments('-DWITH_NVIDIA', language: 'cpp') add_global_arguments('-DWITH_NVIDIA', language: 'cpp')
@ -41,24 +59,30 @@ endif
if get_option('WithSys') if get_option('WithSys')
add_global_arguments('-DWITH_SYS', language: 'cpp') add_global_arguments('-DWITH_SYS', language: 'cpp')
endif endif
if get_option('ForceHyprlandIPC')
add_global_arguments('-DFORCE_HYPRLAND_IPC', language: 'cpp')
endif
add_global_arguments('-DUSE_LOGFILE', language: 'cpp') add_global_arguments('-DUSE_LOGFILE', language: 'cpp')
pulse = dependency('libpulse') pulse = dependency('libpulse')
libgBar = library('gBar', libgBar = library('gBar',
['src/Window.cpp', [ ext_workspace_src,
'src/Widget.cpp', ext_workspace_header,
'src/System.cpp', 'src/Window.cpp',
'src/Bar.cpp', 'src/Widget.cpp',
'src/System.cpp',
'src/Bar.cpp',
'src/Workspaces.cpp',
'src/AudioFlyin.cpp', 'src/AudioFlyin.cpp',
'src/BluetoothDevices.cpp', 'src/BluetoothDevices.cpp',
'src/Plugin.cpp', 'src/Plugin.cpp',
'src/Config.cpp', 'src/Config.cpp',
'src/CSS.cpp', 'src/CSS.cpp',
'src/Log.cpp' 'src/Log.cpp',
], ],
dependencies: [gtk, gtk_layer_shell, pulse], dependencies: [gtk, gtk_layer_shell, pulse, wayland_client],
install: true) install: true)
pkg = import('pkgconfig') pkg = import('pkgconfig')
@ -66,7 +90,7 @@ pkg.generate(libgBar)
executable( executable(
'gBar', 'gBar',
['src/gBar.cpp'], ['src/gBar.cpp'],
dependencies: [gtk], dependencies: [gtk],
link_with: libgBar, link_with: libgBar,
install_rpath: '$ORIGIN/', install_rpath: '$ORIGIN/',

View file

@ -1,7 +1,10 @@
option('WithHyprland', type: 'boolean', value : true) # Deprecated, use WithWorkspaces instead
option('WithHyprland', type: 'boolean', value : false)
option('WithWorkspaces', type: 'boolean', value : true)
option('WithNvidia', type: 'boolean', value : true) option('WithNvidia', type: 'boolean', value : true)
option('WithAMD', type: 'boolean', value : true) option('WithAMD', type: 'boolean', value : true)
option('WithBlueZ', type: 'boolean', value : true) option('WithBlueZ', type: 'boolean', value : true)
option('ForceHyprlandIPC', type: 'boolean', value : false)
# You shouldn't enable this, unless you know what you are doing! # You shouldn't enable this, unless you know what you are doing!
option('WithSys', type: 'boolean', value : false) option('WithSys', type: 'boolean', value : false)

View file

@ -0,0 +1,306 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="ext_workspace_unstable_v1">
<copyright>
Copyright © 2019 Christopher Billington
Copyright © 2020 Ilia Bozhinov
Permission to use, copy, modify, distribute, and sell this
software and its documentation for any purpose is hereby granted
without fee, provided that the above copyright notice appear in
all copies and that both that copyright notice and this permission
notice appear in supporting documentation, and that the name of
the copyright holders not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. The copyright holders make no
representations about the suitability of this software for any
purpose. It is provided "as is" without express or implied
warranty.
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
</copyright>
<interface name="zext_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 zext_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 (workspaces, outputs) will be
sent immediately after this event via the corresponding events in
zext_workspace_group_handle_v1.
</description>
<arg name="workspace_group" type="new_id" interface="zext_workspace_group_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 zext_workspace_handle_v1 objects, for example, deactivating one
workspace and activating another.
</description>
</request>
<event name="done">
<description summary="all information about the workspace groups has been sent">
This event is sent after all changes in all workspace groups have been
sent.
This allows changes to one or more zext_workspace_group_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
zext_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">
<description summary="the compositor has finished with the workspace_manager">
This event indicates that the compositor is done sending events to the
zext_workspace_manager_v1. The server will destroy the object
immediately after sending this request, so it will become invalid and
the client should free any resources associated with it.
</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 client must not send any more requests after this one.
</description>
</request>
</interface>
<interface name="zext_workspace_group_handle_v1" version="1">
<description summary="a workspace group assigned to a set of outputs">
A zext_workspace_group_handle_v1 object represents a 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>
<event name="output_enter">
<description summary="output assigned to workspace group">
This event is emitted whenever an output is assigned to the 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">
<description summary="workspace added to workspace group">
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
zext_workspace_handle_v1.
</description>
<arg name="workspace" type="new_id" interface="zext_workspace_handle_v1"/>
</event>
<event name="remove">
<description summary="this workspace group has been destroyed">
This event means the zext_workspace_group_handle_v1 has been destroyed.
It is guaranteed there won't be any more events for this
zext_workspace_group_handle_v1. The zext_workspace_group_handle_v1 becomes
inert so any requests will be ignored except the destroy request.
The compositor must remove all workspaces belonging to a workspace group
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.
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 zext_workspace_handle_v1 object">
Destroys the zext_workspace_handle_v1 object.
This request should be called 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>
</interface>
<interface name="zext_workspace_handle_v1" version="1">
<description summary="a workspace handing a group of surfaces">
A zext_workspace_handle_v1 object represents a a workspace that handles a
group of surfaces.
Each workspace has a name, conveyed to the client with the name 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.
Depepending 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="name">
<description summary="workspace name changed">
This event is emitted immediately after the zext_workspace_handle_v1 is
created and whenever the name of the workspace changes.
</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 zext_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>
<event name="state">
<description summary="the state of the workspace changed">
This event is emitted immediately after the zext_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.
</description>
<arg name="state" type="array"/>
</event>
<enum name="state">
<description summary="types of states on the workspace">
The different states that a workspace can have.
</description>
<entry name="active" value="0" summary="the workspace is active"/>
<entry name="urgent" value="1" summary="the workspace requests attention"/>
<entry name="hidden" value="2">
<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="remove">
<description summary="this workspace has been destroyed">
This event means the zext_workspace_handle_v1 has been destroyed. It is
guaranteed there won't be any more events for this
zext_workspace_handle_v1. The zext_workspace_handle_v1 becomes inert so
any requests will be ignored except the destroy request.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the zext_workspace_handle_v1 object">
Destroys the zext_workspace_handle_v1 object.
This request should be called 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="activate the workspace">
Request that this workspace be deactivated.
There is no guarantee the workspace will be actually deactivated.
</description>
</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>

View file

@ -198,7 +198,7 @@ namespace Bar
return TimerResult::Ok; return TimerResult::Ok;
} }
#ifdef WITH_HYPRLAND #ifdef WITH_WORKSPACES
static std::array<Button*, 9> workspaces; static std::array<Button*, 9> workspaces;
TimerResult UpdateWorkspaces(Box&) TimerResult UpdateWorkspaces(Box&)
{ {
@ -547,7 +547,7 @@ namespace Bar
parent.AddChild(std::move(eventBox)); parent.AddChild(std::move(eventBox));
} }
#ifdef WITH_HYPRLAND #ifdef WITH_WORKSPACES
void WidgetWorkspaces(Widget& parent) void WidgetWorkspaces(Widget& parent)
{ {
auto margin = Widget::Create<Box>(); auto margin = Widget::Create<Box>();
@ -591,8 +591,8 @@ namespace Bar
auto left = Widget::Create<Box>(); auto left = Widget::Create<Box>();
left->SetSpacing({0, false}); left->SetSpacing({0, false});
left->SetHorizontalTransform({-1, true, Alignment::Left}); left->SetHorizontalTransform({-1, true, Alignment::Left});
#ifdef WITH_HYPRLAND #ifdef WITH_WORKSPACES
if (RuntimeConfig::Get().hasHyprland) if (RuntimeConfig::Get().hasWorkspaces)
{ {
WidgetWorkspaces(*left); WidgetWorkspaces(*left);
} }

View file

@ -33,7 +33,7 @@ public:
static const Config& Get(); static const Config& Get();
}; };
// Configs, that rely on specific files to be available(e.g. Hyprland running) // Configs, that rely on specific files to be available(e.g. BlueZ running)
class RuntimeConfig class RuntimeConfig
{ {
public: public:
@ -48,10 +48,10 @@ public:
bool hasAMD = false; bool hasAMD = false;
#endif #endif
#ifdef WITH_HYPRLAND #ifdef WITH_WORKSPACES
bool hasHyprland = true; bool hasWorkspaces = true;
#else #else
bool hasHyprland = false; bool hasWorkspaces = false;
#endif #endif
#ifdef WITH_BLUEZ #ifdef WITH_BLUEZ

View file

@ -3,7 +3,7 @@
#include "NvidiaGPU.h" #include "NvidiaGPU.h"
#include "AMDGPU.h" #include "AMDGPU.h"
#include "PulseAudio.h" #include "PulseAudio.h"
#include "Hyprland.h" #include "Workspaces.h"
#include "Config.h" #include "Config.h"
#include <cstdlib> #include <cstdlib>
@ -446,22 +446,22 @@ namespace System
PulseAudio::SetVolumeSource(volume); PulseAudio::SetVolumeSource(volume);
} }
#ifdef WITH_HYPRLAND #ifdef WITH_WORKSPACES
void PollWorkspaces(uint32_t monitor, uint32_t numWorkspaces) void PollWorkspaces(uint32_t monitor, uint32_t numWorkspaces)
{ {
Hyprland::PollStatus(monitor, numWorkspaces); Workspaces::PollStatus(monitor, numWorkspaces);
} }
WorkspaceStatus GetWorkspaceStatus(uint32_t workspace) WorkspaceStatus GetWorkspaceStatus(uint32_t workspace)
{ {
return Hyprland::GetStatus(workspace); return Workspaces::GetStatus(workspace);
} }
void GotoWorkspace(uint32_t workspace) void GotoWorkspace(uint32_t workspace)
{ {
return Hyprland::Goto(workspace); return Workspaces::Goto(workspace);
} }
void GotoNextWorkspace(char direction) void GotoNextWorkspace(char direction)
{ {
return Hyprland::GotoNext(direction); return Workspaces::GotoNext(direction);
} }
std::string GetWorkspaceSymbol(int index) std::string GetWorkspaceSymbol(int index)
{ {
@ -582,8 +582,8 @@ namespace System
AMDGPU::Init(); AMDGPU::Init();
#endif #endif
#ifdef WITH_HYPRLAND #ifdef WITH_WORKSPACES
Hyprland::Init(); Workspaces::Init();
#endif #endif
#ifdef WITH_BLUEZ #ifdef WITH_BLUEZ
@ -601,6 +601,10 @@ namespace System
#endif #endif
PulseAudio::Shutdown(); PulseAudio::Shutdown();
#ifdef WITH_WORKSPACES
Workspaces::Shutdown();
#endif
#ifdef WITH_BLUEZ #ifdef WITH_BLUEZ
StopBTScan(); StopBTScan();
#endif #endif

View file

@ -83,7 +83,7 @@ namespace System
void SetVolumeSink(double volume); void SetVolumeSink(double volume);
void SetVolumeSource(double volume); void SetVolumeSource(double volume);
#ifdef WITH_HYPRLAND #ifdef WITH_WORKSPACES
enum class WorkspaceStatus enum class WorkspaceStatus
{ {
Dead, Dead,

283
src/Workspaces.cpp Normal file
View file

@ -0,0 +1,283 @@
#include "Workspaces.h"
#include <unordered_map>
#include <wayland-client.h>
#include <ext-workspace-unstable-v1.h>
#ifdef WITH_WORKSPACES
#ifndef FORCE_HYPRLAND_IPC
namespace Workspaces
{
struct WaylandMonitor
{
std::string name;
wl_output* output;
zext_workspace_group_handle_v1* workspaceGroup;
};
struct WaylandWorkspace
{
zext_workspace_group_handle_v1* parent;
uint32_t id;
bool active;
};
struct WaylandWorkspaceGroup
{
std::vector<zext_workspace_handle_v1*> workspaces;
zext_workspace_handle_v1* lastActiveWorkspace;
};
// There's probably a better way to avoid the LUTs
static std::unordered_map<uint32_t, WaylandMonitor> monitors;
static std::unordered_map<zext_workspace_group_handle_v1*, WaylandWorkspaceGroup> workspaceGroups;
static std::unordered_map<zext_workspace_handle_v1*, WaylandWorkspace> workspaces;
static uint32_t curID = 0;
static wl_display* display;
static wl_registry* registry;
static zext_workspace_manager_v1* workspaceManager;
static bool registeredMonitors = false;
static bool registeredGroup = false;
static bool registeredWorkspace = false;
static bool registeredWorkspaceInfo = false;
// Wayland callbacks
// Workspace Callbacks
static void OnWorkspaceName(void*, zext_workspace_handle_v1* workspace, const char* name)
{
workspaces[workspace].id = std::stoul(name);
LOG("Workspace ID: " << workspaces[workspace].id);
registeredWorkspaceInfo = true;
}
static void OnWorkspaceGeometry(void*, zext_workspace_handle_v1*, wl_array*) {}
static void OnWorkspaceState(void*, zext_workspace_handle_v1* ws, wl_array* arrState)
{
WaylandWorkspace& workspace = workspaces[ws];
ASSERT(workspace.parent, "Wayland: Workspace not registered!");
WaylandWorkspaceGroup& group = workspaceGroups[workspace.parent];
workspace.active = false;
// Manual wl_array_for_each, since that's broken for C++
for (zext_workspace_handle_v1_state* state = (zext_workspace_handle_v1_state*)arrState->data;
(uint8_t*)state < (uint8_t*)arrState->data + arrState->size; state += 1)
{
if (*state == ZEXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE)
{
group.lastActiveWorkspace = ws;
workspace.active = true;
}
}
}
static void OnWorkspaceRemove(void*, zext_workspace_handle_v1* ws)
{
WaylandWorkspace& workspace = workspaces[ws];
ASSERT(workspace.parent, "Wayland: Workspace not registered!");
WaylandWorkspaceGroup& group = workspaceGroups[workspace.parent];
auto it = std::find(group.workspaces.begin(), group.workspaces.end(), ws);
group.workspaces.erase(it);
workspaces.erase(ws);
LOG("Wayland: Removed workspace!");
}
zext_workspace_handle_v1_listener workspaceListener = {OnWorkspaceName, OnWorkspaceGeometry, OnWorkspaceState, OnWorkspaceRemove};
// Workspace Group callbacks
static void OnWSGroupOutputEnter(void*, zext_workspace_group_handle_v1* group, wl_output* output)
{
auto monitor = std::find_if(monitors.begin(), monitors.end(),
[&](const std::pair<uint32_t, WaylandMonitor>& mon)
{
return mon.second.output == output;
});
ASSERT(monitor != monitors.end(), "Wayland: Registered WS group before monitor!");
LOG("Wayland: Added group to monitor");
monitor->second.workspaceGroup = group;
}
static void OnWSGroupOutputLeave(void*, zext_workspace_group_handle_v1*, wl_output* output)
{
auto monitor = std::find_if(monitors.begin(), monitors.end(),
[&](const std::pair<uint32_t, WaylandMonitor>& mon)
{
return mon.second.output == output;
});
ASSERT(monitor != monitors.end(), "Wayland: Registered WS group before monitor!");
LOG("Wayland: Added group to monitor");
monitor->second.workspaceGroup = nullptr;
}
static void OnWSGroupWorkspaceAdded(void*, zext_workspace_group_handle_v1* workspace, zext_workspace_handle_v1* ws)
{
LOG("Wayland: Added workspace!");
workspaceGroups[workspace].workspaces.push_back(ws);
workspaces[ws] = {workspace, (uint32_t)-1};
zext_workspace_handle_v1_add_listener(ws, &workspaceListener, nullptr);
registeredWorkspace = true;
}
static void OnWSGroupRemove(void*, zext_workspace_group_handle_v1* workspaceGroup)
{
workspaceGroups.erase(workspaceGroup);
}
zext_workspace_group_handle_v1_listener workspaceGroupListener = {OnWSGroupOutputEnter, OnWSGroupOutputLeave, OnWSGroupWorkspaceAdded,
OnWSGroupRemove};
// Workspace Manager Callbacks
static void OnWSManagerNewGroup(void*, zext_workspace_manager_v1*, zext_workspace_group_handle_v1* group)
{
// Register callbacks for the group.
registeredGroup = true;
zext_workspace_group_handle_v1_add_listener(group, &workspaceGroupListener, nullptr);
}
static void OnWSManagerDone(void*, zext_workspace_manager_v1*) {}
static void OnWSManagerFinished(void*, zext_workspace_manager_v1*)
{
LOG("Wayland: Workspace manager finished. Disabling workspaces!");
RuntimeConfig::Get().hasWorkspaces = false;
}
zext_workspace_manager_v1_listener workspaceManagerListener = {OnWSManagerNewGroup, OnWSManagerDone, OnWSManagerFinished};
// Output Callbacks
// Very bloated, indeed
static void OnOutputGeometry(void*, wl_output*, int32_t, int32_t, int32_t, int32_t, int32_t, const char*, const char*, int32_t) {}
static void OnOutputMode(void*, wl_output*, uint32_t, int32_t, int32_t, int32_t) {}
static void OnOutputDone(void*, wl_output*) {}
static void OnOutputScale(void*, wl_output*, int32_t) {}
static void OnOutputName(void*, wl_output* output, const char* name)
{
LOG("Wayland: Registering monitor " << name << " at ID " << curID);
registeredMonitors = true;
monitors.try_emplace(curID++, WaylandMonitor{name, output, nullptr});
}
static void OnOutputDescription(void*, wl_output*, const char*) {}
wl_output_listener outputListener = {OnOutputGeometry, OnOutputMode, OnOutputDone, OnOutputScale, OnOutputName, OnOutputDescription};
// Registry Callbacks
static void OnRegistryAdd(void*, wl_registry* registry, uint32_t name, const char* interface, uint32_t version)
{
if (strcmp(interface, "wl_output") == 0)
{
wl_output* output = (wl_output*)wl_registry_bind(registry, name, &wl_output_interface, 4);
wl_output_add_listener(output, &outputListener, nullptr);
}
if (strcmp(interface, "zext_workspace_manager_v1") == 0)
{
workspaceManager = (zext_workspace_manager_v1*)wl_registry_bind(registry, name, &zext_workspace_manager_v1_interface, version);
zext_workspace_manager_v1_add_listener(workspaceManager, &workspaceManagerListener, nullptr);
}
}
static void OnRegistryRemove(void*, wl_registry*, uint32_t) {}
wl_registry_listener registryListener = {OnRegistryAdd, OnRegistryRemove};
// Dispatch events.
static void Dispatch()
{
wl_display_roundtrip(display);
}
static void WaitFor(bool& condition)
{
while (!condition && wl_display_dispatch(display) != -1)
{
}
}
void Init()
{
display = wl_display_connect(nullptr);
ASSERT(display, "Cannot connect to wayland compositor!");
registry = wl_display_get_registry(display);
ASSERT(registry, "Cannot get wayland registry!");
wl_registry_add_listener(registry, &registryListener, nullptr);
wl_display_roundtrip(display);
WaitFor(registeredMonitors);
registeredMonitors = false;
if (!workspaceManager)
{
LOG("Compositor doesn't implement zext_workspace_manager_v1, disabling workspaces!");
RuntimeConfig::Get().hasWorkspaces = false;
return;
}
}
// Hack for unified interface
static uint32_t lastPolledMonitor = 0;
void PollStatus(uint32_t monitor, uint32_t)
{
lastPolledMonitor = monitor;
// Dispatch events
Dispatch();
if (registeredGroup)
{
// New Group, wait for workspaces to be registered.
WaitFor(registeredWorkspace);
}
if (registeredWorkspace)
{
// New workspace added, need info
WaitFor(registeredWorkspaceInfo);
}
registeredGroup = false;
registeredWorkspace = false;
registeredWorkspaceInfo = false;
return;
}
System::WorkspaceStatus GetStatus(uint32_t workspaceId)
{
WaylandMonitor& monitor = monitors[lastPolledMonitor];
if (!monitor.output)
{
LOG("Polled monitor doesn't exist!");
return System::WorkspaceStatus::Dead;
}
auto workspaceIt = std::find_if(workspaces.begin(), workspaces.end(),
[&](const std::pair<zext_workspace_handle_v1*, WaylandWorkspace>& ws)
{
return ws.second.id == workspaceId;
});
if (workspaceIt == workspaces.end())
{
return System::WorkspaceStatus::Dead;
}
WaylandWorkspaceGroup& group = workspaceGroups[monitor.workspaceGroup];
if (group.lastActiveWorkspace)
{
WaylandWorkspace& activeWorkspace = workspaces[group.lastActiveWorkspace];
if (activeWorkspace.id == workspaceId && activeWorkspace.active)
{
return System::WorkspaceStatus::Active;
}
else if (activeWorkspace.id == workspaceId)
{
// Last active workspace (Means we can still see it, since no other ws is active and thus is only visible)
return System::WorkspaceStatus::Current;
}
}
WaylandWorkspaceGroup& currentWorkspaceGroup = workspaceGroups[workspaceIt->second.parent];
if (currentWorkspaceGroup.lastActiveWorkspace == workspaceIt->first)
{
return System::WorkspaceStatus::Visible;
}
else
{
return System::WorkspaceStatus::Inactive;
}
return System::WorkspaceStatus::Dead;
}
void Shutdown()
{
wl_display_disconnect(display);
}
}
#endif
#endif

View file

@ -11,16 +11,17 @@
#include <sys/un.h> #include <sys/un.h>
#include <unistd.h> #include <unistd.h>
#ifdef WITH_HYPRLAND #ifdef WITH_WORKSPACES
namespace Hyprland namespace Workspaces
{ {
#ifdef FORCE_HYPRLAND_IPC
inline void Init() inline void Init()
{ {
if (!getenv("HYPRLAND_INSTANCE_SIGNATURE")) if (!getenv("HYPRLAND_INSTANCE_SIGNATURE"))
{ {
LOG("Hyprland not running, disabling workspaces"); LOG("Workspaces not running, disabling workspaces");
// Not available // Not available
RuntimeConfig::Get().hasHyprland = false; RuntimeConfig::Get().hasWorkspaces = false;
} }
} }
@ -59,9 +60,9 @@ namespace Hyprland
inline void PollStatus(uint32_t monitorID, uint32_t numWorkspaces) inline void PollStatus(uint32_t monitorID, uint32_t numWorkspaces)
{ {
if (RuntimeConfig::Get().hasHyprland == false) if (RuntimeConfig::Get().hasWorkspaces == false)
{ {
LOG("Error: Polled workspace status, but Hyprland isn't open!"); LOG("Error: Polled workspace status, but Workspaces isn't open!");
return; return;
} }
workspaceStati.clear(); workspaceStati.clear();
@ -135,20 +136,32 @@ namespace Hyprland
inline System::WorkspaceStatus GetStatus(uint32_t workspaceId) inline System::WorkspaceStatus GetStatus(uint32_t workspaceId)
{ {
if (RuntimeConfig::Get().hasHyprland == false) if (RuntimeConfig::Get().hasWorkspaces == false)
{ {
LOG("Error: Queried for workspace status, but Hyprland isn't open!"); LOG("Error: Queried for workspace status, but Workspaces isn't open!");
return System::WorkspaceStatus::Dead; return System::WorkspaceStatus::Dead;
} }
ASSERT(workspaceId > 0 && workspaceId <= workspaceStati.size(), "Invalid workspaceId, you need to poll the workspace first!"); ASSERT(workspaceId > 0 && workspaceId <= workspaceStati.size(), "Invalid workspaceId, you need to poll the workspace first!");
return workspaceStati[workspaceId - 1]; return workspaceStati[workspaceId - 1];
} }
inline void Shutdown() {}
#else
void Init();
void PollStatus(uint32_t monitorID, uint32_t numWorkspaces);
System::WorkspaceStatus GetStatus(uint32_t workspaceId);
void Shutdown();
#endif
// TODO: Use ext_workspaces for this
inline void Goto(uint32_t workspace) inline void Goto(uint32_t workspace)
{ {
if (RuntimeConfig::Get().hasHyprland == false) if (RuntimeConfig::Get().hasWorkspaces == false)
{ {
LOG("Error: Called Go to workspace, but Hyprland isn't open!"); LOG("Error: Called Go to workspace, but Workspaces isn't open!");
return; return;
} }