diff --git a/src/app/application.cpp b/src/app/application.cpp index 089d1dfe8..a4dfccc01 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -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 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); } diff --git a/src/app/application.h b/src/app/application.h index 784b246dd..d36847b94 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -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 m_brightnessPollSource; std::unique_ptr m_pipewirePollSource; diff --git a/src/compositors/compositor_platform.cpp b/src/compositors/compositor_platform.cpp index 3058cb1d1..d760202c8 100644 --- a/src/compositors/compositor_platform.cpp +++ b/src/compositors/compositor_platform.cpp @@ -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 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& 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& 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); } diff --git a/src/compositors/compositor_platform.h b/src/compositors/compositor_platform.h index b41eab5d6..62befe987 100644 --- a/src/compositors/compositor_platform.h +++ b/src/compositors/compositor_platform.h @@ -102,6 +102,10 @@ public: [[nodiscard]] std::string currentKeyboardLayoutName() const; [[nodiscard]] std::vector keyboardLayoutNames() const; + void setKeyboardLayoutChangeCallback(ChangeCallback callback); + void addKeyboardLayoutPollFds(std::vector& fds) const; + void dispatchKeyboardLayoutPoll(const std::vector& fds, std::size_t startIdx); + [[nodiscard]] bool setOutputPower(bool on) const; [[nodiscard]] bool tracksOverviewState() const noexcept; @@ -132,6 +136,7 @@ private: std::unique_ptr m_outputPowerBackend; std::unique_ptr m_keyboardLayoutBackend; ChangeCallback m_workspaceChangeCallback; + ChangeCallback m_keyboardLayoutChangeCallback; std::vector m_lastWorkspaceModelSnapshot; bool m_initialized = false; }; diff --git a/src/compositors/hyprland/hyprland_keyboard_backend.cpp b/src/compositors/hyprland/hyprland_keyboard_backend.cpp index 2a96b7495..b78a33c53 100644 --- a/src/compositors/hyprland/hyprland_keyboard_backend.cpp +++ b/src/compositors/hyprland/hyprland_keyboard_backend.cpp @@ -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 +#include +#include #include -#include -#include +#include +#include +#include +#include +#include +#include #include -#include namespace { - constexpr auto kCurrentLayoutCacheTtl = std::chrono::seconds(1); - - struct CurrentLayoutCache { - std::optional 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 runAndCapture(const std::vector& 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 argv; - argv.reserve(args.size() + 1); - for (const auto& arg : args) { - argv.push_back(const_cast(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(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 HyprlandKeyboardBackend::layoutState() const { @@ -107,49 +48,278 @@ std::optional HyprlandKeyboardBackend::layoutState() const } std::optional 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 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(&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(&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(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 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(); + } } diff --git a/src/compositors/hyprland/hyprland_keyboard_backend.h b/src/compositors/hyprland/hyprland_keyboard_backend.h index 5f7e0639a..69866399b 100644 --- a/src/compositors/hyprland/hyprland_keyboard_backend.h +++ b/src/compositors/hyprland/hyprland_keyboard_backend.h @@ -2,19 +2,49 @@ #include "compositors/keyboard_backend.h" +#include +#include #include #include #include +#include class HyprlandKeyboardBackend { public: + using ChangeCallback = std::function; + 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 layoutState() const; [[nodiscard]] std::optional 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 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 m_readBuffer; + ChangeCallback m_changeCallback; }; diff --git a/src/compositors/keyboard_backend.h b/src/compositors/keyboard_backend.h index 75540b3eb..dccc37c74 100644 --- a/src/compositors/keyboard_backend.h +++ b/src/compositors/keyboard_backend.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include @@ -11,10 +13,19 @@ struct KeyboardLayoutState { class KeyboardLayoutBackend { public: + using ChangeCallback = std::function; + virtual ~KeyboardLayoutBackend() = default; [[nodiscard]] virtual bool isAvailable() const noexcept = 0; [[nodiscard]] virtual bool cycleLayout() const = 0; [[nodiscard]] virtual std::optional layoutState() const = 0; [[nodiscard]] virtual std::optional 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() {} }; diff --git a/src/wayland/keyboard_layout_poll_source.h b/src/wayland/keyboard_layout_poll_source.h new file mode 100644 index 000000000..2ca317653 --- /dev/null +++ b/src/wayland/keyboard_layout_poll_source.h @@ -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& fds, std::size_t startIdx) override { + m_platform.dispatchKeyboardLayoutPoll(fds, startIdx); + } + +protected: + void doAddPollFds(std::vector& fds) override { m_platform.addKeyboardLayoutPollFds(fds); } + +private: + CompositorPlatform& m_platform; +};