mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(compositor): migrate workspace tracking from niri IPC to ext-workspace protocol
This commit is contained in:
+23
-1
@@ -47,6 +47,8 @@ set(XDG_OUTPUT_XML
|
||||
"${WAYLAND_PROTOCOLS_PKGDATADIR}/unstable/xdg-output/xdg-output-unstable-v1.xml")
|
||||
set(XDG_SHELL_XML
|
||||
"${WAYLAND_PROTOCOLS_PKGDATADIR}/stable/xdg-shell/xdg-shell.xml")
|
||||
set(EXT_WORKSPACE_XML
|
||||
"${WAYLAND_PROTOCOLS_PKGDATADIR}/staging/ext-workspace/ext-workspace-v1.xml")
|
||||
|
||||
set(XDG_OUTPUT_PROTOCOL_C
|
||||
"${GENERATED_PROTOCOL_DIR}/xdg-output-unstable-v1-client-protocol.c")
|
||||
@@ -56,6 +58,10 @@ set(XDG_SHELL_PROTOCOL_C
|
||||
"${GENERATED_PROTOCOL_DIR}/xdg-shell-client-protocol.c")
|
||||
set(XDG_SHELL_PROTOCOL_H
|
||||
"${GENERATED_PROTOCOL_DIR}/xdg-shell-client-protocol.h")
|
||||
set(EXT_WORKSPACE_PROTOCOL_C
|
||||
"${GENERATED_PROTOCOL_DIR}/ext-workspace-v1-client-protocol.c")
|
||||
set(EXT_WORKSPACE_PROTOCOL_H
|
||||
"${GENERATED_PROTOCOL_DIR}/ext-workspace-v1-client-protocol.h")
|
||||
|
||||
set(WLR_LAYER_SHELL_PROTOCOL_C
|
||||
"${GENERATED_PROTOCOL_DIR}/wlr-layer-shell-unstable-v1-client-protocol.c")
|
||||
@@ -116,6 +122,20 @@ add_custom_command(
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${EXT_WORKSPACE_PROTOCOL_C}"
|
||||
COMMAND "${WAYLAND_SCANNER_EXECUTABLE}" private-code "${EXT_WORKSPACE_XML}" "${EXT_WORKSPACE_PROTOCOL_C}"
|
||||
DEPENDS "${EXT_WORKSPACE_XML}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${EXT_WORKSPACE_PROTOCOL_H}"
|
||||
COMMAND "${WAYLAND_SCANNER_EXECUTABLE}" client-header "${EXT_WORKSPACE_XML}" "${EXT_WORKSPACE_PROTOCOL_H}"
|
||||
DEPENDS "${EXT_WORKSPACE_XML}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(noctalia_wayland_protocols
|
||||
DEPENDS
|
||||
"${WLR_LAYER_SHELL_PROTOCOL_C}"
|
||||
@@ -124,6 +144,8 @@ add_custom_target(noctalia_wayland_protocols
|
||||
"${XDG_OUTPUT_PROTOCOL_H}"
|
||||
"${XDG_SHELL_PROTOCOL_C}"
|
||||
"${XDG_SHELL_PROTOCOL_H}"
|
||||
"${EXT_WORKSPACE_PROTOCOL_C}"
|
||||
"${EXT_WORKSPACE_PROTOCOL_H}"
|
||||
)
|
||||
|
||||
# --- Target ---
|
||||
@@ -131,7 +153,6 @@ add_executable(noctalia
|
||||
src/main.cpp
|
||||
src/app/Application.cpp
|
||||
src/app/MainLoop.cpp
|
||||
src/compositor/niri/NiriCompositorService.cpp
|
||||
src/system/SystemMonitorService.cpp
|
||||
src/debug/DebugService.cpp
|
||||
src/notification/InternalNotificationService.cpp
|
||||
@@ -161,6 +182,7 @@ add_executable(noctalia
|
||||
"${WLR_LAYER_SHELL_PROTOCOL_C}"
|
||||
"${XDG_OUTPUT_PROTOCOL_C}"
|
||||
"${XDG_SHELL_PROTOCOL_C}"
|
||||
"${EXT_WORKSPACE_PROTOCOL_C}"
|
||||
)
|
||||
target_compile_definitions(noctalia PRIVATE NOCTALIA_HAVE_WLR_LAYER_SHELL=1)
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ Everything else depends on these.
|
||||
|
||||
### Phase 2 -- Minimum viable bar
|
||||
|
||||
- [x] Compositor integration (Niri)
|
||||
- [x] Compositor integration (ext-workspace)
|
||||
- [ ] Compositor integration (Hyprland)
|
||||
- [ ] Workspaces widget
|
||||
- [ ] Clock widget
|
||||
|
||||
@@ -45,16 +45,6 @@ void Application::run() {
|
||||
m_systemMonitor.reset();
|
||||
}
|
||||
|
||||
try {
|
||||
m_niriCompositor = std::make_unique<NiriService>();
|
||||
if (m_niriCompositor->isRunning()) {
|
||||
logInfo("niri compositor integration active");
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
logWarn("niri compositor integration disabled: {}", e.what());
|
||||
m_niriCompositor.reset();
|
||||
}
|
||||
|
||||
try {
|
||||
m_bus = std::make_unique<SessionBus>();
|
||||
logInfo("connected to session bus");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/MainLoop.hpp"
|
||||
#include "compositor/niri/NiriCompositorService.hpp"
|
||||
#include "debug/DebugService.hpp"
|
||||
#include "dbus/SessionBus.hpp"
|
||||
#include "dbus/mpris/MprisService.hpp"
|
||||
@@ -27,7 +26,6 @@ private:
|
||||
Bar m_bar;
|
||||
std::unique_ptr<SessionBus> m_bus;
|
||||
std::unique_ptr<SystemMonitorService> m_systemMonitor;
|
||||
std::unique_ptr<NiriService> m_niriCompositor;
|
||||
std::unique_ptr<DebugService> m_debugService;
|
||||
std::unique_ptr<MprisService> m_mprisService;
|
||||
NotificationManager m_manager;
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
#include "compositor/niri/NiriCompositorService.hpp"
|
||||
|
||||
#include "core/Log.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view k_event_stream_request = "\"EventStream\"\n";
|
||||
|
||||
} // namespace
|
||||
|
||||
NiriService::NiriService() {
|
||||
if (!connectSocket()) {
|
||||
return;
|
||||
}
|
||||
startReader();
|
||||
}
|
||||
|
||||
NiriService::~NiriService() {
|
||||
stopReader();
|
||||
}
|
||||
|
||||
bool NiriService::isRunning() const noexcept {
|
||||
return m_running.load();
|
||||
}
|
||||
|
||||
bool NiriService::connectSocket() {
|
||||
const char* socket_path_env = std::getenv("NIRI_SOCKET");
|
||||
if (socket_path_env == nullptr || socket_path_env[0] == '\0') {
|
||||
logInfo("niri integration disabled: NIRI_SOCKET is not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string socket_path{socket_path_env};
|
||||
if (socket_path.size() >= sizeof(sockaddr_un::sun_path)) {
|
||||
logWarn("niri integration disabled: NIRI_SOCKET path is too long");
|
||||
return false;
|
||||
}
|
||||
|
||||
const int fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
logWarn("niri integration disabled: failed to create unix socket: {}", std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
sockaddr_un addr{};
|
||||
addr.sun_family = AF_UNIX;
|
||||
std::strncpy(addr.sun_path, socket_path.c_str(), sizeof(addr.sun_path) - 1);
|
||||
|
||||
if (::connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
|
||||
logWarn("niri integration disabled: failed to connect to {}: {}", socket_path, std::strerror(errno));
|
||||
::close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
const ssize_t wrote = ::send(fd,
|
||||
k_event_stream_request.data(),
|
||||
k_event_stream_request.size(),
|
||||
MSG_NOSIGNAL);
|
||||
if (wrote != static_cast<ssize_t>(k_event_stream_request.size())) {
|
||||
logWarn("niri integration disabled: failed to request event stream");
|
||||
::close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_socket_fd = fd;
|
||||
logInfo("niri integration connected (event stream)");
|
||||
return true;
|
||||
}
|
||||
|
||||
void NiriService::startReader() {
|
||||
if (m_socket_fd < 0 || m_running.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_running = true;
|
||||
m_reader_thread = std::thread([this]() {
|
||||
readerLoop();
|
||||
});
|
||||
}
|
||||
|
||||
void NiriService::stopReader() {
|
||||
m_running = false;
|
||||
|
||||
if (m_socket_fd >= 0) {
|
||||
::shutdown(m_socket_fd, SHUT_RDWR);
|
||||
::close(m_socket_fd);
|
||||
m_socket_fd = -1;
|
||||
}
|
||||
|
||||
if (m_reader_thread.joinable()) {
|
||||
m_reader_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void NiriService::readerLoop() {
|
||||
std::string line;
|
||||
while (m_running.load() && readLine(m_socket_fd, line)) {
|
||||
if (line.empty()) {
|
||||
continue;
|
||||
}
|
||||
handleEventLine(line);
|
||||
}
|
||||
|
||||
if (m_running.load()) {
|
||||
logWarn("niri event stream closed");
|
||||
}
|
||||
}
|
||||
|
||||
bool NiriService::readLine(int fd, std::string& out) {
|
||||
out.clear();
|
||||
|
||||
const auto emit_from_buffer = [&]() -> bool {
|
||||
const std::size_t newline_pos = m_read_buffer.find('\n');
|
||||
if (newline_pos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out = m_read_buffer.substr(0, newline_pos);
|
||||
m_read_buffer.erase(0, newline_pos + 1);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (emit_from_buffer()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<char, 256> buffer{};
|
||||
while (true) {
|
||||
const ssize_t n = ::recv(fd, buffer.data(), buffer.size(), 0);
|
||||
if (n == 0) {
|
||||
if (!m_read_buffer.empty()) {
|
||||
out = std::move(m_read_buffer);
|
||||
m_read_buffer.clear();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
m_read_buffer.append(buffer.data(), static_cast<std::size_t>(n));
|
||||
if (emit_from_buffer()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NiriService::handleEventLine(const std::string& line) {
|
||||
if (line.find("\"Err\"") != std::string::npos) {
|
||||
logWarn("niri event stream returned error: {}", line);
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.find("\"WorkspacesChanged\"") != std::string::npos) {
|
||||
handleWorkspacesChanged(line);
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.find("\"WorkspaceActivated\"") != std::string::npos) {
|
||||
handleWorkspaceActivated(line);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void NiriService::handleWorkspacesChanged(const std::string& line) {
|
||||
const std::size_t key_pos = line.find("\"workspaces\"");
|
||||
if (key_pos == std::string::npos) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t array_open = line.find('[', key_pos);
|
||||
if (array_open == std::string::npos) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t array_close = findMatchingBracket(line, array_open);
|
||||
if (array_close == std::string::npos) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_workspace_labels.clear();
|
||||
|
||||
std::optional<std::uint64_t> focused_id;
|
||||
std::size_t cursor = array_open + 1;
|
||||
while (cursor < array_close) {
|
||||
const std::size_t obj_open = line.find('{', cursor);
|
||||
if (obj_open == std::string::npos || obj_open >= array_close) {
|
||||
break;
|
||||
}
|
||||
const std::size_t obj_close = findMatchingBrace(line, obj_open);
|
||||
if (obj_close == std::string::npos || obj_close > array_close) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto id = parseJsonUintField(line, obj_open, "\"id\"");
|
||||
const auto idx = parseJsonUintField(line, obj_open, "\"idx\"");
|
||||
const auto name = parseJsonStringField(line, obj_open, "\"name\"");
|
||||
const auto output = parseJsonStringField(line, obj_open, "\"output\"");
|
||||
|
||||
if (id.has_value()) {
|
||||
std::string label;
|
||||
if (name.has_value() && !name->empty()) {
|
||||
label = *name;
|
||||
} else if (idx.has_value()) {
|
||||
label = std::format("{}", *idx);
|
||||
} else {
|
||||
label = std::format("id {}", *id);
|
||||
}
|
||||
|
||||
if (output.has_value() && !output->empty()) {
|
||||
label = std::format("{}@{}", label, *output);
|
||||
}
|
||||
|
||||
m_workspace_labels[*id] = std::move(label);
|
||||
}
|
||||
|
||||
const std::size_t focused_pos = line.find("\"is_focused\"", obj_open);
|
||||
if (focused_pos != std::string::npos && focused_pos < obj_close) {
|
||||
const std::size_t true_pos = line.find("true", focused_pos);
|
||||
if (true_pos != std::string::npos && true_pos < obj_close && id.has_value()) {
|
||||
focused_id = *id;
|
||||
}
|
||||
}
|
||||
|
||||
cursor = obj_close + 1;
|
||||
}
|
||||
|
||||
if (focused_id.has_value()) {
|
||||
logFocusedWorkspace(*focused_id);
|
||||
}
|
||||
}
|
||||
|
||||
void NiriService::handleWorkspaceActivated(const std::string& line) {
|
||||
const std::size_t focused_pos = line.find("\"focused\"");
|
||||
if (focused_pos == std::string::npos) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t true_pos = line.find("true", focused_pos);
|
||||
if (true_pos == std::string::npos) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto id = parseJsonUintField(line, 0, "\"id\"");
|
||||
if (id.has_value()) {
|
||||
logFocusedWorkspace(*id);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> NiriService::parseJsonStringField(const std::string& input,
|
||||
std::size_t from,
|
||||
const char* key) {
|
||||
const std::size_t key_pos = input.find(key, from);
|
||||
if (key_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::size_t colon = input.find(':', key_pos);
|
||||
if (colon == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::size_t first = input.find_first_not_of(" \t", colon + 1);
|
||||
if (first == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (input.compare(first, 4, "null") == 0) {
|
||||
return std::string{};
|
||||
}
|
||||
|
||||
if (input[first] != '"') {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
++first;
|
||||
const std::size_t end = input.find('"', first);
|
||||
if (end == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return input.substr(first, end - first);
|
||||
}
|
||||
|
||||
std::optional<std::uint64_t> NiriService::parseJsonUintField(const std::string& input,
|
||||
std::size_t from,
|
||||
const char* key) {
|
||||
const std::size_t key_pos = input.find(key, from);
|
||||
if (key_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::size_t colon = input.find(':', key_pos);
|
||||
if (colon == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::size_t first = input.find_first_not_of(" \t", colon + 1);
|
||||
if (first == std::string::npos || first >= input.size() || !std::isdigit(static_cast<unsigned char>(input[first]))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::size_t last = first;
|
||||
while (last < input.size() && std::isdigit(static_cast<unsigned char>(input[last]))) {
|
||||
++last;
|
||||
}
|
||||
|
||||
try {
|
||||
return static_cast<std::uint64_t>(std::stoull(input.substr(first, last - first)));
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t NiriService::findMatchingBracket(const std::string& input, std::size_t open_pos) {
|
||||
int depth = 0;
|
||||
for (std::size_t i = open_pos; i < input.size(); ++i) {
|
||||
if (input[i] == '[') {
|
||||
++depth;
|
||||
} else if (input[i] == ']') {
|
||||
--depth;
|
||||
if (depth == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
std::size_t NiriService::findMatchingBrace(const std::string& input, std::size_t open_pos) {
|
||||
int depth = 0;
|
||||
for (std::size_t i = open_pos; i < input.size(); ++i) {
|
||||
if (input[i] == '{') {
|
||||
++depth;
|
||||
} else if (input[i] == '}') {
|
||||
--depth;
|
||||
if (depth == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
void NiriService::logFocusedWorkspace(std::uint64_t id) {
|
||||
if (m_last_focused_workspace.has_value() && *m_last_focused_workspace == id) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_last_focused_workspace = id;
|
||||
|
||||
const auto it = m_workspace_labels.find(id);
|
||||
if (it != m_workspace_labels.end()) {
|
||||
logInfo("niri focused workspace {}", it->second);
|
||||
return;
|
||||
}
|
||||
|
||||
logInfo("niri focused workspace id {}", id);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
class NiriService {
|
||||
public:
|
||||
NiriService();
|
||||
~NiriService();
|
||||
|
||||
NiriService(const NiriService&) = delete;
|
||||
NiriService& operator=(const NiriService&) = delete;
|
||||
|
||||
[[nodiscard]] bool isRunning() const noexcept;
|
||||
|
||||
private:
|
||||
bool connectSocket();
|
||||
void startReader();
|
||||
void stopReader();
|
||||
void readerLoop();
|
||||
|
||||
bool readLine(int fd, std::string& out);
|
||||
void handleEventLine(const std::string& line);
|
||||
void handleWorkspacesChanged(const std::string& line);
|
||||
void handleWorkspaceActivated(const std::string& line);
|
||||
|
||||
static std::optional<std::string> parseJsonStringField(const std::string& input,
|
||||
std::size_t from,
|
||||
const char* key);
|
||||
static std::optional<std::uint64_t> parseJsonUintField(const std::string& input,
|
||||
std::size_t from,
|
||||
const char* key);
|
||||
static std::size_t findMatchingBracket(const std::string& input, std::size_t open_pos);
|
||||
static std::size_t findMatchingBrace(const std::string& input, std::size_t open_pos);
|
||||
|
||||
void logFocusedWorkspace(std::uint64_t id);
|
||||
|
||||
int m_socket_fd{-1};
|
||||
std::atomic<bool> m_running{false};
|
||||
std::thread m_reader_thread;
|
||||
std::string m_read_buffer;
|
||||
std::unordered_map<std::uint64_t, std::string> m_workspace_labels;
|
||||
std::optional<std::uint64_t> m_last_focused_workspace;
|
||||
};
|
||||
@@ -10,6 +10,7 @@
|
||||
#if NOCTALIA_HAVE_WLR_LAYER_SHELL
|
||||
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
|
||||
#endif
|
||||
#include "ext-workspace-v1-client-protocol.h"
|
||||
#include "xdg-output-unstable-v1-client-protocol.h"
|
||||
|
||||
namespace {
|
||||
@@ -21,6 +22,7 @@ constexpr std::uint32_t kShmVersion = 1;
|
||||
constexpr std::uint32_t kLayerShellVersion = 4;
|
||||
#endif
|
||||
constexpr std::uint32_t kXdgOutputManagerVersion = 3;
|
||||
constexpr std::uint32_t kExtWorkspaceManagerVersion = 1;
|
||||
constexpr std::uint32_t kOutputVersion = 4;
|
||||
|
||||
const wl_registry_listener kRegistryListener = {
|
||||
@@ -79,6 +81,83 @@ const wl_output_listener kOutputListener = {
|
||||
.description = outputDescription,
|
||||
};
|
||||
|
||||
void workspaceGroupCapabilities(void* /*data*/, ext_workspace_group_handle_v1* /*group*/, uint32_t /*caps*/) {}
|
||||
void workspaceGroupOutputEnter(void* /*data*/, ext_workspace_group_handle_v1* /*group*/, wl_output* /*output*/) {}
|
||||
void workspaceGroupOutputLeave(void* /*data*/, ext_workspace_group_handle_v1* /*group*/, wl_output* /*output*/) {}
|
||||
void workspaceGroupWorkspaceEnter(void* /*data*/, ext_workspace_group_handle_v1* /*group*/, ext_workspace_handle_v1* /*workspace*/) {}
|
||||
void workspaceGroupWorkspaceLeave(void* /*data*/, ext_workspace_group_handle_v1* /*group*/, ext_workspace_handle_v1* /*workspace*/) {}
|
||||
void workspaceGroupRemoved(void* data, ext_workspace_group_handle_v1* group) {
|
||||
auto* self = static_cast<WaylandConnection*>(data);
|
||||
self->onWorkspaceGroupRemoved(group);
|
||||
}
|
||||
|
||||
const ext_workspace_group_handle_v1_listener kWorkspaceGroupListener = {
|
||||
.capabilities = workspaceGroupCapabilities,
|
||||
.output_enter = workspaceGroupOutputEnter,
|
||||
.output_leave = workspaceGroupOutputLeave,
|
||||
.workspace_enter = workspaceGroupWorkspaceEnter,
|
||||
.workspace_leave = workspaceGroupWorkspaceLeave,
|
||||
.removed = workspaceGroupRemoved,
|
||||
};
|
||||
|
||||
void workspaceId(void* /*data*/, ext_workspace_handle_v1* /*workspace*/, const char* /*id*/) {}
|
||||
|
||||
void workspaceName(void* data, ext_workspace_handle_v1* workspace, const char* name) {
|
||||
auto* self = static_cast<WaylandConnection*>(data);
|
||||
self->onWorkspaceNameChanged(workspace, name);
|
||||
}
|
||||
|
||||
void workspaceCoordinates(void* /*data*/, ext_workspace_handle_v1* /*workspace*/, wl_array* /*coords*/) {}
|
||||
|
||||
void workspaceState(void* data, ext_workspace_handle_v1* workspace, uint32_t state) {
|
||||
auto* self = static_cast<WaylandConnection*>(data);
|
||||
self->onWorkspaceStateChanged(workspace, state);
|
||||
}
|
||||
|
||||
void workspaceCapabilities(void* /*data*/, ext_workspace_handle_v1* /*workspace*/, uint32_t /*caps*/) {}
|
||||
|
||||
void workspaceRemoved(void* data, ext_workspace_handle_v1* workspace) {
|
||||
auto* self = static_cast<WaylandConnection*>(data);
|
||||
self->onWorkspaceRemoved(workspace);
|
||||
}
|
||||
|
||||
const ext_workspace_handle_v1_listener kWorkspaceListener = {
|
||||
.id = workspaceId,
|
||||
.name = workspaceName,
|
||||
.coordinates = workspaceCoordinates,
|
||||
.state = workspaceState,
|
||||
.capabilities = workspaceCapabilities,
|
||||
.removed = workspaceRemoved,
|
||||
};
|
||||
|
||||
void workspaceManagerWorkspaceGroup(void* data,
|
||||
ext_workspace_manager_v1* /*manager*/,
|
||||
ext_workspace_group_handle_v1* group) {
|
||||
auto* self = static_cast<WaylandConnection*>(data);
|
||||
self->onWorkspaceGroupCreated(group);
|
||||
}
|
||||
|
||||
void workspaceManagerWorkspace(void* data,
|
||||
ext_workspace_manager_v1* /*manager*/,
|
||||
ext_workspace_handle_v1* workspace) {
|
||||
auto* self = static_cast<WaylandConnection*>(data);
|
||||
self->onWorkspaceCreated(workspace);
|
||||
}
|
||||
|
||||
void workspaceManagerDone(void* /*data*/, ext_workspace_manager_v1* /*manager*/) {}
|
||||
|
||||
void workspaceManagerFinished(void* data, ext_workspace_manager_v1* /*manager*/) {
|
||||
auto* self = static_cast<WaylandConnection*>(data);
|
||||
self->onWorkspaceManagerFinished();
|
||||
}
|
||||
|
||||
const ext_workspace_manager_v1_listener kWorkspaceManagerListener = {
|
||||
.workspace_group = workspaceManagerWorkspaceGroup,
|
||||
.workspace = workspaceManagerWorkspace,
|
||||
.done = workspaceManagerDone,
|
||||
.finished = workspaceManagerFinished,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
WaylandConnection::WaylandConnection() = default;
|
||||
@@ -142,6 +221,10 @@ bool WaylandConnection::hasXdgOutputManager() const noexcept {
|
||||
return m_xdgOutputManager != nullptr;
|
||||
}
|
||||
|
||||
bool WaylandConnection::hasExtWorkspaceManager() const noexcept {
|
||||
return m_workspaceManager != nullptr;
|
||||
}
|
||||
|
||||
wl_display* WaylandConnection::display() const noexcept {
|
||||
return m_display;
|
||||
}
|
||||
@@ -228,6 +311,15 @@ void WaylandConnection::bindGlobal(wl_registry* registry,
|
||||
return;
|
||||
}
|
||||
|
||||
if (interfaceName == ext_workspace_manager_v1_interface.name) {
|
||||
m_hasExtWorkspaceGlobal = true;
|
||||
const auto bindVersion = std::min(version, kExtWorkspaceManagerVersion);
|
||||
m_workspaceManager = static_cast<ext_workspace_manager_v1*>(
|
||||
wl_registry_bind(registry, name, &ext_workspace_manager_v1_interface, bindVersion));
|
||||
ext_workspace_manager_v1_add_listener(m_workspaceManager, &kWorkspaceManagerListener, this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (interfaceName == wl_output_interface.name) {
|
||||
const auto bindVersion = std::min(version, kOutputVersion);
|
||||
auto* output = static_cast<wl_output*>(
|
||||
@@ -255,7 +347,87 @@ WaylandOutput* WaylandConnection::findOutputByWl(wl_output* wlOutput) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void WaylandConnection::onWorkspaceGroupCreated(ext_workspace_group_handle_v1* group) {
|
||||
if (group == nullptr) {
|
||||
return;
|
||||
}
|
||||
m_workspaceGroups.push_back(group);
|
||||
ext_workspace_group_handle_v1_add_listener(group, &kWorkspaceGroupListener, this);
|
||||
}
|
||||
|
||||
void WaylandConnection::onWorkspaceGroupRemoved(ext_workspace_group_handle_v1* group) {
|
||||
std::erase(m_workspaceGroups, group);
|
||||
if (group != nullptr) {
|
||||
ext_workspace_group_handle_v1_destroy(group);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandConnection::onWorkspaceCreated(ext_workspace_handle_v1* workspace) {
|
||||
if (workspace == nullptr) {
|
||||
return;
|
||||
}
|
||||
m_workspaces.emplace(workspace, TrackedWorkspace{});
|
||||
ext_workspace_handle_v1_add_listener(workspace, &kWorkspaceListener, this);
|
||||
}
|
||||
|
||||
void WaylandConnection::onWorkspaceNameChanged(ext_workspace_handle_v1* workspace, const char* name) {
|
||||
const auto it = m_workspaces.find(workspace);
|
||||
if (it == m_workspaces.end()) {
|
||||
return;
|
||||
}
|
||||
it->second.name = name != nullptr ? name : "";
|
||||
}
|
||||
|
||||
void WaylandConnection::onWorkspaceStateChanged(ext_workspace_handle_v1* workspace, std::uint32_t state) {
|
||||
const auto it = m_workspaces.find(workspace);
|
||||
if (it == m_workspaces.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool is_active = (state & EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE) != 0;
|
||||
if (it->second.active != is_active) {
|
||||
it->second.active = is_active;
|
||||
if (is_active) {
|
||||
const std::string label = it->second.name.empty() ? "(unnamed)" : it->second.name;
|
||||
logInfo("workspace active: {}", label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandConnection::onWorkspaceRemoved(ext_workspace_handle_v1* workspace) {
|
||||
m_workspaces.erase(workspace);
|
||||
if (workspace != nullptr) {
|
||||
ext_workspace_handle_v1_destroy(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
void WaylandConnection::onWorkspaceManagerFinished() {
|
||||
m_workspaceManager = nullptr;
|
||||
m_workspaces.clear();
|
||||
m_workspaceGroups.clear();
|
||||
}
|
||||
|
||||
void WaylandConnection::cleanup() {
|
||||
for (auto& [workspace, _] : m_workspaces) {
|
||||
if (workspace != nullptr) {
|
||||
ext_workspace_handle_v1_destroy(workspace);
|
||||
}
|
||||
}
|
||||
m_workspaces.clear();
|
||||
|
||||
for (auto* group : m_workspaceGroups) {
|
||||
if (group != nullptr) {
|
||||
ext_workspace_group_handle_v1_destroy(group);
|
||||
}
|
||||
}
|
||||
m_workspaceGroups.clear();
|
||||
|
||||
if (m_workspaceManager != nullptr) {
|
||||
ext_workspace_manager_v1_stop(m_workspaceManager);
|
||||
ext_workspace_manager_v1_destroy(m_workspaceManager);
|
||||
m_workspaceManager = nullptr;
|
||||
}
|
||||
|
||||
if (m_xdgOutputManager != nullptr) {
|
||||
zxdg_output_manager_v1_destroy(m_xdgOutputManager);
|
||||
m_xdgOutputManager = nullptr;
|
||||
@@ -301,14 +473,16 @@ void WaylandConnection::cleanup() {
|
||||
}
|
||||
|
||||
m_outputs.clear();
|
||||
m_hasExtWorkspaceGlobal = false;
|
||||
}
|
||||
|
||||
void WaylandConnection::logStartupSummary() const {
|
||||
logInfo("wayland connected compositor={} shm={} layer-shell={} xdg-output={} outputs={}",
|
||||
logInfo("wayland connected compositor={} shm={} layer-shell={} xdg-output={} ext-workspace={} outputs={}",
|
||||
m_compositor != nullptr ? "yes" : "no",
|
||||
m_shm != nullptr ? "yes" : "no",
|
||||
hasLayerShell() ? "yes" : "no",
|
||||
hasXdgOutputManager() ? "yes" : "no",
|
||||
hasExtWorkspaceManager() ? "yes" : "no",
|
||||
m_outputs.size());
|
||||
|
||||
for (const auto& output : m_outputs) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct wl_compositor;
|
||||
@@ -13,6 +14,9 @@ struct wl_seat;
|
||||
struct wl_shm;
|
||||
struct zwlr_layer_shell_v1;
|
||||
struct zxdg_output_manager_v1;
|
||||
struct ext_workspace_manager_v1;
|
||||
struct ext_workspace_group_handle_v1;
|
||||
struct ext_workspace_handle_v1;
|
||||
|
||||
struct WaylandOutput {
|
||||
std::uint32_t name = 0;
|
||||
@@ -43,6 +47,7 @@ public:
|
||||
bool hasRequiredGlobals() const noexcept;
|
||||
bool hasLayerShell() const noexcept;
|
||||
bool hasXdgOutputManager() const noexcept;
|
||||
bool hasExtWorkspaceManager() const noexcept;
|
||||
wl_display* display() const noexcept;
|
||||
wl_compositor* compositor() const noexcept;
|
||||
wl_shm* shm() const noexcept;
|
||||
@@ -59,6 +64,23 @@ public:
|
||||
std::uint32_t name);
|
||||
|
||||
private:
|
||||
struct TrackedWorkspace {
|
||||
std::string name;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
public:
|
||||
// Internal callback entrypoints used by C listeners for ext-workspace.
|
||||
void onWorkspaceGroupCreated(ext_workspace_group_handle_v1* group);
|
||||
void onWorkspaceGroupRemoved(ext_workspace_group_handle_v1* group);
|
||||
void onWorkspaceCreated(ext_workspace_handle_v1* workspace);
|
||||
void onWorkspaceNameChanged(ext_workspace_handle_v1* workspace, const char* name);
|
||||
void onWorkspaceStateChanged(ext_workspace_handle_v1* workspace, std::uint32_t state);
|
||||
void onWorkspaceRemoved(ext_workspace_handle_v1* workspace);
|
||||
void onWorkspaceManagerFinished();
|
||||
|
||||
private:
|
||||
|
||||
void bindGlobal(wl_registry* registry,
|
||||
std::uint32_t name,
|
||||
const char* interface,
|
||||
@@ -73,7 +95,11 @@ private:
|
||||
wl_shm* m_shm = nullptr;
|
||||
zwlr_layer_shell_v1* m_layerShell = nullptr;
|
||||
zxdg_output_manager_v1* m_xdgOutputManager = nullptr;
|
||||
ext_workspace_manager_v1* m_workspaceManager = nullptr;
|
||||
bool m_hasLayerShellGlobal = false;
|
||||
bool m_hasExtWorkspaceGlobal = false;
|
||||
std::vector<WaylandOutput> m_outputs;
|
||||
std::vector<ext_workspace_group_handle_v1*> m_workspaceGroups;
|
||||
std::unordered_map<ext_workspace_handle_v1*, TrackedWorkspace> m_workspaces;
|
||||
OutputChangeCallback m_outputChangeCallback;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user