hyprland: more IPC usage and dont use hyprctl for kb

This commit is contained in:
Lemmy
2026-05-10 00:23:51 -04:00
parent afc7f7f58a
commit 0e5c3ee882
8 changed files with 405 additions and 117 deletions
+2
View File
@@ -380,6 +380,7 @@ void Application::initServices() {
}
});
m_compositorPlatform.setWorkspaceChangeCallback([this]() { m_bar.refresh(); });
m_compositorPlatform.setKeyboardLayoutChangeCallback([this]() { m_bar.refresh(); });
m_wayland.setToplevelChangeCallback([this]() {
m_bar.refresh();
m_dock.refresh();
@@ -1302,6 +1303,7 @@ std::vector<PollSource*> Application::currentPollSources() {
sources.push_back(&m_timerPollSource);
sources.push_back(&m_keyRepeatPollSource);
sources.push_back(&m_workspacePollSource);
sources.push_back(&m_keyboardLayoutPollSource);
if constexpr (kLockKeysEnabled) {
sources.push_back(&m_lockKeysPollSource);
}
+2
View File
@@ -85,6 +85,7 @@
#include "wayland/clipboard_poll_source.h"
#include "wayland/clipboard_service.h"
#include "wayland/key_repeat_poll_source.h"
#include "wayland/keyboard_layout_poll_source.h"
#include "wayland/virtual_keyboard_service.h"
#include "wayland/wayland_connection.h"
#include "wayland/workspace_poll_source.h"
@@ -202,6 +203,7 @@ private:
TimerPollSource m_timerPollSource;
KeyRepeatPollSource m_keyRepeatPollSource{m_wayland};
WorkspacePollSource m_workspacePollSource{m_compositorPlatform};
KeyboardLayoutPollSource m_keyboardLayoutPollSource{m_compositorPlatform};
LockKeysPollSource m_lockKeysPollSource{m_lockKeysService};
std::unique_ptr<BrightnessPollSource> m_brightnessPollSource;
std::unique_ptr<PipeWirePollSource> m_pipewirePollSource;
+49
View File
@@ -91,6 +91,34 @@ namespace {
return m_backend.currentLayoutName();
}
bool connectSocket() override {
if constexpr (requires { m_backend.connectSocket(); }) {
return m_backend.connectSocket();
}
return false;
}
void setChangeCallback(ChangeCallback callback) override {
if constexpr (requires { m_backend.setChangeCallback(std::move(callback)); }) {
m_backend.setChangeCallback(std::move(callback));
}
}
[[nodiscard]] int pollFd() const noexcept override {
if constexpr (requires { m_backend.pollFd(); }) {
return m_backend.pollFd();
}
return -1;
}
void dispatchPoll(short revents) override {
if constexpr (requires { m_backend.dispatchPoll(revents); }) {
m_backend.dispatchPoll(revents);
}
}
void cleanup() override {
if constexpr (requires { m_backend.cleanup(); }) {
m_backend.cleanup();
}
}
private:
BackendT m_backend;
};
@@ -532,6 +560,27 @@ std::vector<std::string> CompositorPlatform::keyboardLayoutNames() const {
return m_wayland.keyboardLayoutNames();
}
void CompositorPlatform::setKeyboardLayoutChangeCallback(ChangeCallback callback) {
m_keyboardLayoutChangeCallback = std::move(callback);
if (m_keyboardLayoutBackend != nullptr) {
m_keyboardLayoutBackend->setChangeCallback(m_keyboardLayoutChangeCallback);
m_keyboardLayoutBackend->connectSocket();
}
}
void CompositorPlatform::addKeyboardLayoutPollFds(std::vector<pollfd>& fds) const {
if (m_keyboardLayoutBackend != nullptr && m_keyboardLayoutBackend->pollFd() >= 0) {
fds.push_back(
{.fd = m_keyboardLayoutBackend->pollFd(), .events = m_keyboardLayoutBackend->pollEvents(), .revents = 0});
}
}
void CompositorPlatform::dispatchKeyboardLayoutPoll(const std::vector<pollfd>& fds, std::size_t startIdx) {
if (m_keyboardLayoutBackend != nullptr && m_keyboardLayoutBackend->pollFd() >= 0 && startIdx < fds.size()) {
m_keyboardLayoutBackend->dispatchPoll(fds[startIdx].revents);
}
}
bool CompositorPlatform::setOutputPower(bool on) const {
return m_outputPowerBackend != nullptr && m_outputPowerBackend->setOutputPower(m_wayland, on);
}
+5
View File
@@ -102,6 +102,10 @@ public:
[[nodiscard]] std::string currentKeyboardLayoutName() const;
[[nodiscard]] std::vector<std::string> keyboardLayoutNames() const;
void setKeyboardLayoutChangeCallback(ChangeCallback callback);
void addKeyboardLayoutPollFds(std::vector<pollfd>& fds) const;
void dispatchKeyboardLayoutPoll(const std::vector<pollfd>& fds, std::size_t startIdx);
[[nodiscard]] bool setOutputPower(bool on) const;
[[nodiscard]] bool tracksOverviewState() const noexcept;
@@ -132,6 +136,7 @@ private:
std::unique_ptr<compositors::OutputPowerBackend> m_outputPowerBackend;
std::unique_ptr<KeyboardLayoutBackend> m_keyboardLayoutBackend;
ChangeCallback m_workspaceChangeCallback;
ChangeCallback m_keyboardLayoutChangeCallback;
std::vector<WorkspaceModelSnapshot> m_lastWorkspaceModelSnapshot;
bool m_initialized = false;
};
@@ -1,80 +1,22 @@
#include "compositors/hyprland/hyprland_keyboard_backend.h"
#include "core/process.h"
#include "core/log.h"
#include "util/string_utils.h"
#include <chrono>
#include <algorithm>
#include <cerrno>
#include <cstdlib>
#include <json.hpp>
#include <sys/wait.h>
#include <cstring>
#include <fcntl.h>
#include <filesystem>
#include <string_view>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <vector>
namespace {
constexpr auto kCurrentLayoutCacheTtl = std::chrono::seconds(1);
struct CurrentLayoutCache {
std::optional<std::string> value;
std::chrono::steady_clock::time_point fetchedAt{};
bool valid = false;
};
CurrentLayoutCache& currentLayoutCache() {
static CurrentLayoutCache cache;
return cache;
}
void invalidateCurrentLayoutCache() { currentLayoutCache() = CurrentLayoutCache{}; }
[[nodiscard]] std::optional<std::string> runAndCapture(const std::vector<std::string>& args) {
if (args.empty() || args.front().empty()) {
return std::nullopt;
}
int pipefd[2];
if (::pipe(pipefd) != 0) {
return std::nullopt;
}
const pid_t pid = ::fork();
if (pid < 0) {
::close(pipefd[0]);
::close(pipefd[1]);
return std::nullopt;
}
if (pid == 0) {
::close(pipefd[0]);
::dup2(pipefd[1], STDOUT_FILENO);
::close(pipefd[1]);
std::vector<char*> argv;
argv.reserve(args.size() + 1);
for (const auto& arg : args) {
argv.push_back(const_cast<char*>(arg.c_str()));
}
argv.push_back(nullptr);
::execvp(argv[0], argv.data());
::_exit(127);
}
::close(pipefd[1]);
std::string output;
char buffer[4096];
ssize_t count = 0;
while ((count = ::read(pipefd[0], buffer, sizeof(buffer))) > 0) {
output.append(buffer, static_cast<std::size_t>(count));
}
::close(pipefd[0]);
int status = 0;
if (::waitpid(pid, &status, 0) < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) {
return std::nullopt;
}
return output;
}
constexpr Logger kLog("keyboard_hyprland");
} // namespace
@@ -85,17 +27,16 @@ HyprlandKeyboardBackend::HyprlandKeyboardBackend(std::string_view compositorHint
m_enabled = hinted || (signature != nullptr && signature[0] != '\0');
}
HyprlandKeyboardBackend::~HyprlandKeyboardBackend() { cleanup(); }
bool HyprlandKeyboardBackend::isAvailable() const noexcept { return m_enabled; }
bool HyprlandKeyboardBackend::cycleLayout() const {
if (!m_enabled) {
if (!m_enabled || m_requestSocketPath.empty()) {
return false;
}
const bool ok = process::runSync({"hyprctl", "switchxkblayout", "all", "next"});
if (ok) {
invalidateCurrentLayoutCache();
}
return ok;
std::string response;
return sendRequest("switchxkblayout all next", response);
}
std::optional<KeyboardLayoutState> HyprlandKeyboardBackend::layoutState() const {
@@ -107,49 +48,278 @@ std::optional<KeyboardLayoutState> HyprlandKeyboardBackend::layoutState() const
}
std::optional<std::string> HyprlandKeyboardBackend::currentLayoutName() const {
if (!m_enabled) {
if (!m_enabled || m_currentLayoutName.empty()) {
return std::nullopt;
}
const auto now = std::chrono::steady_clock::now();
auto& cache = currentLayoutCache();
if (cache.valid && now - cache.fetchedAt < kCurrentLayoutCacheTtl) {
return cache.value;
}
auto finish = [&](std::optional<std::string> value) {
cache.value = std::move(value);
cache.fetchedAt = now;
cache.valid = true;
return cache.value;
};
const auto payload = runAndCapture({"hyprctl", "devices", "-j"});
if (!payload.has_value() || payload->empty()) {
return finish(std::nullopt);
}
try {
const auto json = nlohmann::json::parse(*payload);
const auto keyboardsIt = json.find("keyboards");
if (keyboardsIt == json.end() || !keyboardsIt->is_array()) {
return finish(std::nullopt);
}
for (const auto& keyboard : *keyboardsIt) {
if (!keyboard.is_object()) {
continue;
}
if (!keyboard.value("main", false)) {
continue;
}
const std::string layout = keyboard.value("active_keymap", "");
if (!layout.empty() && layout != "error") {
return finish(layout);
}
}
} catch (const nlohmann::json::exception&) {
return finish(std::nullopt);
}
return finish(std::nullopt);
return m_currentLayoutName;
}
bool HyprlandKeyboardBackend::connectSocket() {
if (!m_enabled || !ensureSocketPaths()) {
return false;
}
cleanup();
m_eventSocketFd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (m_eventSocketFd < 0) {
kLog.warn("failed to create hyprland keyboard IPC socket: {}", std::strerror(errno));
return false;
}
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
if (m_eventSocketPath.size() >= sizeof(addr.sun_path)) {
kLog.warn("hyprland keyboard IPC socket path too long");
cleanup();
return false;
}
std::memcpy(addr.sun_path, m_eventSocketPath.c_str(), m_eventSocketPath.size() + 1);
if (::connect(m_eventSocketFd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
kLog.warn("failed to connect to hyprland keyboard IPC {}: {}", m_eventSocketPath, std::strerror(errno));
cleanup();
return false;
}
const int flags = ::fcntl(m_eventSocketFd, F_GETFL, 0);
if (flags >= 0) {
(void)::fcntl(m_eventSocketFd, F_SETFL, flags | O_NONBLOCK);
}
seedLayoutFromDevices();
kLog.info("connected to hyprland keyboard IPC at {}", m_eventSocketPath);
return true;
}
void HyprlandKeyboardBackend::setChangeCallback(ChangeCallback callback) { m_changeCallback = std::move(callback); }
void HyprlandKeyboardBackend::dispatchPoll(short revents) {
if (m_eventSocketFd < 0) {
return;
}
if ((revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
kLog.warn("hyprland keyboard IPC disconnected");
cleanup();
if (m_changeCallback) {
m_changeCallback();
}
return;
}
if ((revents & POLLIN) != 0) {
readSocket();
}
}
void HyprlandKeyboardBackend::cleanup() {
if (m_eventSocketFd >= 0) {
::close(m_eventSocketFd);
m_eventSocketFd = -1;
}
m_readBuffer.clear();
m_currentLayoutName.clear();
m_mainKeyboardName.clear();
}
bool HyprlandKeyboardBackend::ensureSocketPaths() {
if (!m_requestSocketPath.empty() && !m_eventSocketPath.empty()) {
return true;
}
const char* signature = std::getenv("HYPRLAND_INSTANCE_SIGNATURE");
if (signature == nullptr || signature[0] == '\0') {
return false;
}
std::string hyprDir;
const char* runtimeDir = std::getenv("XDG_RUNTIME_DIR");
if (runtimeDir != nullptr && runtimeDir[0] != '\0') {
hyprDir = std::string(runtimeDir) + "/hypr/" + signature;
}
if (hyprDir.empty() || !std::filesystem::is_directory(hyprDir)) {
hyprDir = std::string("/tmp/hypr/") + signature;
}
if (!std::filesystem::is_directory(hyprDir)) {
return false;
}
m_requestSocketPath = hyprDir + "/.socket.sock";
m_eventSocketPath = hyprDir + "/.socket2.sock";
return true;
}
bool HyprlandKeyboardBackend::sendRequest(const std::string& request, std::string& response) const {
if (m_requestSocketPath.empty()) {
return false;
}
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) {
return false;
}
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
if (m_requestSocketPath.size() >= sizeof(addr.sun_path)) {
::close(fd);
return false;
}
std::memcpy(addr.sun_path, m_requestSocketPath.c_str(), m_requestSocketPath.size() + 1);
if (::connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
::close(fd);
return false;
}
std::size_t offset = 0;
while (offset < request.size()) {
const ssize_t written = ::send(fd, request.data() + offset, request.size() - offset, MSG_NOSIGNAL);
if (written < 0) {
if (errno == EINTR) {
continue;
}
::close(fd);
return false;
}
offset += static_cast<std::size_t>(written);
}
::shutdown(fd, SHUT_WR);
std::string out;
char buffer[4096];
while (true) {
const ssize_t n = ::recv(fd, buffer, sizeof(buffer), 0);
if (n > 0) {
out.append(buffer, buffer + n);
continue;
}
if (n == 0) {
break;
}
if (errno == EINTR) {
continue;
}
::close(fd);
return false;
}
::close(fd);
response = std::move(out);
return true;
}
std::optional<nlohmann::json> HyprlandKeyboardBackend::requestJson(const std::string& request) const {
std::string response;
if (!sendRequest(request, response) || response.empty()) {
return std::nullopt;
}
try {
return nlohmann::json::parse(response);
} catch (const nlohmann::json::exception& e) {
kLog.warn("failed to parse hyprland response for {}: {}", request, e.what());
return std::nullopt;
}
}
void HyprlandKeyboardBackend::seedLayoutFromDevices() {
const auto json = requestJson("j/devices");
if (!json || !json->is_object()) {
return;
}
const auto keyboardsIt = json->find("keyboards");
if (keyboardsIt == json->end() || !keyboardsIt->is_array()) {
return;
}
for (const auto& keyboard : *keyboardsIt) {
if (!keyboard.is_object() || !keyboard.value("main", false)) {
continue;
}
const std::string layout = keyboard.value("active_keymap", "");
if (!layout.empty() && layout != "error") {
m_currentLayoutName = layout;
m_mainKeyboardName = keyboard.value("name", "");
return;
}
}
}
void HyprlandKeyboardBackend::readSocket() {
char buffer[4096];
while (true) {
const ssize_t n = ::recv(m_eventSocketFd, buffer, sizeof(buffer), MSG_DONTWAIT);
if (n > 0) {
m_readBuffer.insert(m_readBuffer.end(), buffer, buffer + n);
continue;
}
if (n == 0) {
cleanup();
if (m_changeCallback) {
m_changeCallback();
}
return;
}
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
if (errno == EINTR) {
continue;
}
kLog.warn("failed to read from hyprland keyboard IPC: {}", std::strerror(errno));
cleanup();
if (m_changeCallback) {
m_changeCallback();
}
return;
}
parseMessages();
}
void HyprlandKeyboardBackend::parseMessages() {
while (true) {
auto it = std::find(m_readBuffer.begin(), m_readBuffer.end(), '\n');
if (it == m_readBuffer.end()) {
return;
}
std::string line(m_readBuffer.begin(), it);
m_readBuffer.erase(m_readBuffer.begin(), it + 1);
if (!line.empty()) {
handleEvent(line);
}
}
}
void HyprlandKeyboardBackend::handleEvent(std::string_view line) {
const auto split = line.find(">>");
if (split == std::string_view::npos) {
return;
}
const std::string_view event = line.substr(0, split);
if (event != "activelayout") {
return;
}
const std::string_view data = line.substr(split + 2);
const auto comma = data.find(',');
if (comma == std::string_view::npos || comma + 1 >= data.size()) {
return;
}
const std::string_view keyboardName = data.substr(0, comma);
const std::string_view layoutName = data.substr(comma + 1);
if (!m_mainKeyboardName.empty() && keyboardName != m_mainKeyboardName) {
return;
}
m_currentLayoutName = std::string(layoutName);
if (m_changeCallback) {
m_changeCallback();
}
}
@@ -2,19 +2,49 @@
#include "compositors/keyboard_backend.h"
#include <functional>
#include <json.hpp>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
class HyprlandKeyboardBackend {
public:
using ChangeCallback = std::function<void()>;
explicit HyprlandKeyboardBackend(std::string_view compositorHint);
~HyprlandKeyboardBackend();
HyprlandKeyboardBackend(const HyprlandKeyboardBackend&) = delete;
HyprlandKeyboardBackend& operator=(const HyprlandKeyboardBackend&) = delete;
[[nodiscard]] bool isAvailable() const noexcept;
[[nodiscard]] bool cycleLayout() const;
[[nodiscard]] std::optional<KeyboardLayoutState> layoutState() const;
[[nodiscard]] std::optional<std::string> currentLayoutName() const;
bool connectSocket();
void setChangeCallback(ChangeCallback callback);
[[nodiscard]] int pollFd() const noexcept { return m_eventSocketFd; }
void dispatchPoll(short revents);
void cleanup();
private:
bool ensureSocketPaths();
[[nodiscard]] bool sendRequest(const std::string& request, std::string& response) const;
[[nodiscard]] std::optional<nlohmann::json> requestJson(const std::string& request) const;
void seedLayoutFromDevices();
void readSocket();
void parseMessages();
void handleEvent(std::string_view line);
bool m_enabled = false;
int m_eventSocketFd = -1;
std::string m_requestSocketPath;
std::string m_eventSocketPath;
std::string m_currentLayoutName;
std::string m_mainKeyboardName;
std::vector<char> m_readBuffer;
ChangeCallback m_changeCallback;
};
+11
View File
@@ -1,6 +1,8 @@
#pragma once
#include <functional>
#include <optional>
#include <poll.h>
#include <string>
#include <vector>
@@ -11,10 +13,19 @@ struct KeyboardLayoutState {
class KeyboardLayoutBackend {
public:
using ChangeCallback = std::function<void()>;
virtual ~KeyboardLayoutBackend() = default;
[[nodiscard]] virtual bool isAvailable() const noexcept = 0;
[[nodiscard]] virtual bool cycleLayout() const = 0;
[[nodiscard]] virtual std::optional<KeyboardLayoutState> layoutState() const = 0;
[[nodiscard]] virtual std::optional<std::string> currentLayoutName() const = 0;
virtual bool connectSocket() { return false; }
virtual void setChangeCallback(ChangeCallback /*callback*/) {}
[[nodiscard]] virtual int pollFd() const noexcept { return -1; }
[[nodiscard]] virtual short pollEvents() const noexcept { return POLLIN | POLLHUP | POLLERR; }
virtual void dispatchPoll(short /*revents*/) {}
virtual void cleanup() {}
};
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "app/poll_source.h"
#include "compositors/compositor_platform.h"
class KeyboardLayoutPollSource final : public PollSource {
public:
explicit KeyboardLayoutPollSource(CompositorPlatform& platform) : m_platform(platform) {}
void dispatch(const std::vector<pollfd>& fds, std::size_t startIdx) override {
m_platform.dispatchKeyboardLayoutPoll(fds, startIdx);
}
protected:
void doAddPollFds(std::vector<pollfd>& fds) override { m_platform.addKeyboardLayoutPollFds(fds); }
private:
CompositorPlatform& m_platform;
};