diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e8b9f3a8..a1b316df 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: steps: - run: echo 'http://dl-cdn.alpinelinux.org/alpine/v3.22/community' > /etc/apk/repositories - run: echo 'http://dl-cdn.alpinelinux.org/alpine/v3.22/main' >> /etc/apk/repositories - - run: apk --no-cache add git g++ binutils pkgconf meson ninja musl-dev gtkmm4-dev vala gobject-introspection gobject-introspection-dev pulseaudio-dev libdbusmenu-glib-dev alsa-lib-dev + - run: apk --no-cache add git g++ binutils pkgconf meson ninja musl-dev gtkmm4-dev vala gobject-introspection gobject-introspection-dev pulseaudio-dev libdbusmenu-glib-dev alsa-lib-dev yyjson-dev - run: echo 'http://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories - run: echo 'http://dl-cdn.alpinelinux.org/alpine/edge/main' >> /etc/apk/repositories - run: apk --no-cache add wayland-protocols wayfire-dev gtk4-layer-shell-dev gtk4-layer-shell diff --git a/.gitmodules b/.gitmodules index 0e49778e..1644112b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "subprojects/gtk4-layer-shell"] path = subprojects/gtk4-layer-shell url = https://github.com/wmww/gtk4-layer-shell.git +[submodule "subprojects/wf-json"] + path = subprojects/wf-json + url = https://github.com/WayfireWM/wf-json diff --git a/data/css/default.css b/data/css/default.css index 2479f10c..5f4792aa 100644 --- a/data/css/default.css +++ b/data/css/default.css @@ -61,6 +61,17 @@ padding-bottom: 5px; } +.wf-panel .language { + min-width: 48px; +} + +.wf-panel .language label { + background-color: #24283B; + padding: 5px; + margin: 5px; + border-radius: 3px; + color: #41A6B5; +} .excellent { color: #00ff00; } diff --git a/dependencies/wayfire-upstream b/dependencies/wayfire-upstream new file mode 160000 index 00000000..60231d5d --- /dev/null +++ b/dependencies/wayfire-upstream @@ -0,0 +1 @@ +Subproject commit 60231d5d8e7e4e5261894cea098b2c688726544f diff --git a/meson.build b/meson.build index f8a3207d..f50bcee9 100644 --- a/meson.build +++ b/meson.build @@ -23,6 +23,8 @@ gtklayershell = dependency('gtk4-layer-shell-0', fallback: ['gtk4-layer-shell']) libpulse = dependency('libpulse', required: get_option('pulse')) dbusmenu_gtk = dependency('dbusmenu-glib-0.4') libgvc = subproject('gvc', default_options: ['static=true'], required: get_option('pulse')) +xkbregistry = dependency('xkbregistry') +json = subproject('wf-json').get_variable('wfjson') if get_option('wayland-logout') == true wayland_logout = subproject('wayland-logout') diff --git a/src/panel/meson.build b/src/panel/meson.build index 8b84ac41..d2c4b058 100644 --- a/src/panel/meson.build +++ b/src/panel/meson.build @@ -3,6 +3,7 @@ widget_sources = [ 'widgets/menu.cpp', 'widgets/clock.cpp', 'widgets/command-output.cpp', + 'widgets/language.cpp', 'widgets/launchers.cpp', 'widgets/network.cpp', 'widgets/spacing.cpp', @@ -29,6 +30,8 @@ deps = [ wf_protos, wfconfig, dbusmenu_gtk, + xkbregistry, + json, ] if libpulse.found() diff --git a/src/panel/panel.cpp b/src/panel/panel.cpp index d684d17c..34bdc513 100644 --- a/src/panel/panel.cpp +++ b/src/panel/panel.cpp @@ -7,14 +7,17 @@ #include #include +#include #include #include #include #include "panel.hpp" +#include "wf-ipc.hpp" #include "widgets/battery.hpp" #include "widgets/command-output.hpp" +#include "widgets/language.hpp" #include "widgets/menu.hpp" #include "widgets/clock.hpp" #include "widgets/launchers.hpp" @@ -184,6 +187,11 @@ class WayfirePanel::impl return Widget(new WfCommandOutputButtons()); } + if (name == "language") + { + return Widget(new WayfireLanguage()); + } + if (auto pixel = widget_with_value(name, "spacing")) { return Widget(new WayfireSpacing(*pixel)); @@ -318,6 +326,7 @@ class WayfirePanel::impl WayfirePanel::WayfirePanel(WayfireOutput *output) : pimpl(new impl(output)) {} + wl_surface*WayfirePanel::get_wl_surface() { return pimpl->get_wl_surface(); diff --git a/src/panel/widgets/language.cpp b/src/panel/widgets/language.cpp new file mode 100644 index 00000000..c0d925fa --- /dev/null +++ b/src/panel/widgets/language.cpp @@ -0,0 +1,124 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "language.hpp" +#include "gtkmm/button.h" +#include "sigc++/functors/mem_fun.h" +#include "wf-ipc.hpp" + +void WayfireLanguage::init(Gtk::Box *container) +{ + button.get_style_context()->add_class("language"); + button.get_style_context()->add_class("flat"); + button.get_style_context()->remove_class("activated"); + button.signal_clicked().connect(sigc::mem_fun(*this, &WayfireLanguage::next_layout)); + button.show(); + + ipc_client->subscribe(this, {"keyboard-modifier-state-changed"}); + ipc_client->send("{\"method\":\"wayfire/get-keyboard-state\"}", [=] (wf::json_t data) + { + set_available(data["possible-layouts"]); + set_current(data["layout-index"]); + }); + + container->append(button); +} + +void WayfireLanguage::on_event(wf::json_t data) +{ + if (data["event"].as_string() == "keyboard-modifier-state-changed") + { + if (available_layouts.size() == 0) + { + set_available(data["state"]["possible-layouts"]); + } + + auto state_layout = data["state"]["layout-index"].as_uint(); + if (state_layout != current_layout) + { + current_layout = state_layout; + set_current(state_layout); + } + } +} + +bool WayfireLanguage::update_label() +{ + if (current_layout >= available_layouts.size()) + { + return false; + } + + button.set_label(available_layouts[current_layout].ID); + return true; +} + +void WayfireLanguage::set_current(uint32_t index) +{ + current_layout = index; + update_label(); +} + +void WayfireLanguage::set_available(wf::json_t layouts) +{ + std::vector layouts_available; + std::map names; + + for (size_t i = 0; i < layouts.size(); i++) + { + auto elem = layouts[i]; + names[elem] = i; + layouts_available.push_back(Layout{ + .Name = (std::string)elem, + .ID = "", + }); + } + + auto context = rxkb_context_new(RXKB_CONTEXT_NO_FLAGS); + rxkb_context_parse_default_ruleset(context); + auto rlayout = rxkb_layout_first(context); + for (; rlayout != NULL; rlayout = rxkb_layout_next(rlayout)) + { + auto descr = rxkb_layout_get_description(rlayout); + auto name = names.find(descr); + if (name != names.end()) + { + layouts_available[name->second].ID = rxkb_layout_get_brief(rlayout); + } + } + + available_layouts = layouts_available; + update_label(); +} + +void WayfireLanguage::next_layout() +{ + uint32_t next = current_layout + 1; + if (next >= available_layouts.size()) + { + next = 0; + } + + wf::json_t message; + message["method"] = "wayfire/set-keyboard-state"; + message["data"] = wf::json_t(); + message["data"]["layout-index"] = next; + ipc_client->send(message.serialize()); +} + +WayfireLanguage::WayfireLanguage() +{ + ipc_client = WayfireIPC::get_instance()->create_client(); +} + +WayfireLanguage::~WayfireLanguage() +{ + ipc_client->unsubscribe(this); +} diff --git a/src/panel/widgets/language.hpp b/src/panel/widgets/language.hpp new file mode 100644 index 00000000..a1819724 --- /dev/null +++ b/src/panel/widgets/language.hpp @@ -0,0 +1,40 @@ +#ifndef WIDGETS_LANGUAGE_HPP +#define WIDGETS_LANGUAGE_HPP + +#include "../widget.hpp" +#include "gtkmm/button.h" +#include "wf-ipc.hpp" +#include +#include +#include +#include +#include +#include + +struct Layout +{ + std::string Name; + std::string ID; +}; + +class WayfireLanguage : public WayfireWidget, public IIPCSubscriber +{ + // Gtk::Label label; + Gtk::Button button; + + std::shared_ptr ipc_client; + uint32_t current_layout; + std::vector available_layouts; + + public: + void init(Gtk::Box *container); + void on_event(wf::json_t data) override; + bool update_label(); + void set_current(uint32_t index); + void set_available(wf::json_t layouts); + void next_layout(); + WayfireLanguage(); + ~WayfireLanguage(); +}; + +#endif /* end of include guard: WIDGETS_LANGUAGE_HPP */ diff --git a/src/util/meson.build b/src/util/meson.build index 4bd8174e..cdffc3de 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -6,8 +6,9 @@ util = static_library( 'wf-autohide-window.cpp', 'wf-popover.cpp', 'css-config.cpp', + 'wf-ipc.cpp', ], - dependencies: [wf_protos, gtklayershell, wayland_client, gtkmm, wfconfig, libinotify], + dependencies: [wf_protos, gtklayershell, wayland_client, gtkmm, wfconfig, libinotify, json], ) util_includes = include_directories('.') diff --git a/src/util/wf-ipc.cpp b/src/util/wf-ipc.cpp new file mode 100644 index 00000000..f5ee8735 --- /dev/null +++ b/src/util/wf-ipc.cpp @@ -0,0 +1,377 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wf-ipc.hpp" +#include "giomm/cancellable.h" +#include "giomm/error.h" +#include "giomm/socketclient.h" +#include "giomm/unixsocketaddress.h" +#include "glibconfig.h" +#include "glibmm/error.h" +#include "glibmm/iochannel.h" +#include "glibmm/main.h" +#include "sigc++/functors/mem_fun.h" + +WayfireIPC::WayfireIPC() +{ + connect(); + + sig_connection = Glib::signal_io().connect( + sigc::mem_fun(*this, &WayfireIPC::receive), + connection->get_socket()->get_fd(), + Glib::IOCondition::IO_IN); +} + +WayfireIPC::~WayfireIPC() +{ + disconnect(); +} + +void WayfireIPC::connect() +{ + const char *socket_path = getenv("WAYFIRE_SOCKET"); + if (socket_path == nullptr) + { + throw std::runtime_error("Wayfire socket not found"); + } + + auto client = Gio::SocketClient::create(); + auto address = Gio::UnixSocketAddress::create(socket_path); + connection = client->connect(address); + connection->get_socket()->set_blocking(false); + output = connection->get_output_stream(); + input = connection->get_input_stream(); + cancel = Gio::Cancellable::create(); +} + +void WayfireIPC::disconnect() +{ + cancel->cancel(); + sig_connection.disconnect(); + connection->close(); +} + +void WayfireIPC::send(const std::string& message) +{ + send_message(message); + response_handlers.push(0); +} + +void WayfireIPC::send(const std::string& message, int response_handler) +{ + send_message(message); + response_handlers.push(response_handler); +} + +void WayfireIPC::send_message(const std::string& message) +{ + if (output->has_pending()) + { + write_queue.push(message); + write_next(); + return; + } + + // Shortcut: stream is not busy, no queue needed + write_stream(message); +} + +void WayfireIPC::write_next() +{ + if (writing || cancel->is_cancelled()) + { + return; + } + + writing = true; + sig_connection = Glib::signal_io().connect( + sigc::mem_fun(*this, &WayfireIPC::send_queue), + connection->get_socket()->get_fd(), + Glib::IOCondition::IO_OUT); +} + +void WayfireIPC::write_stream(const std::string& message) +{ + try { + writing = true; + uint32_t length = message.size(); + // Pointer to data must be valid until completely wrote and + // slot is called, as documented for write_all_async. + // So we pin it with a shared pointer, destroyed *after* slot is called. + auto all_data = std::make_shared((char*)&length, 4); + *all_data += message; + output->write_all_async(all_data->data(), all_data->size(), + [this, all_data] (Glib::RefPtr& result) + { + try { + gsize written; + auto success = output->write_all_finish(result, written); + if (!success) + { + LOGE("IPC error: write failed. Bytes written: ", written); + } + + this->writing = false; + if (!cancel->is_cancelled()) + { + write_next(); + } + } catch (const Glib::Error& e) + { + if (e.code() == G_IO_ERROR_CANCELLED) + { + // Intended behavior + return; + } else + { + LOGE("IPC error: write failed: ", e.what()); + } + } + }, cancel); + } catch (const Gio::Error& e) + { + LOGE("IPC error: ", e.what()); + } +} + +bool WayfireIPC::send_queue(Glib::IOCondition cond) +{ + if (write_queue.empty()) + { + writing = false; + return false; + } + + auto message = write_queue.front(); + write_queue.pop(); + + write_stream(message); + return false; +} + +bool WayfireIPC::receive(Glib::IOCondition cond) +{ + try { + ssize_t received = 0; + uint32_t length; + + // TODO: Input buffer can(?) contain incomplete message + while (connection->get_socket()->get_available_bytes() > 0) + { + received = input->read(&length, sizeof(length)); + if (received == -1) + { + LOGE("IPC error: Receive message length failed"); + return false; + } + + if (received == 0) + { + LOGE("IPC error: Disconnected"); + return false; + } + + if (received != sizeof(length)) + { + LOGE("IPC error: failed to read message. Expected (bytes): ", + sizeof(length), + ", was read (bytes)", + received); + return false; + } + + std::string buf(length, 0); + received = input->read(&buf[0], length); + if (received == -1) + { + LOGE("IPC error: receive message body failed"); + return false; + } + + if (received == 0) + { + LOGE("IPC error: Disconnected"); + return false; + } + + if (received != length) + { + LOGE("IPC error: failed to read message. Expected (bytes): ", + length, + ", was read (bytes)", + received); + return false; + } + + wf::json_t message; + auto err = wf::json_t::parse_string(buf, message); + if (err.has_value()) + { + LOGE("IPC error: JSON parse: ", err.value(), " message: ", buf, " length: ", buf.length()); + return false; + } + + if (message.has_member("event")) + { + for (auto subscriber : subscribers) + { + subscriber->on_event(message); + } + + if (subscriptions.find(message["event"]) != subscriptions.end()) + { + for (auto sub : subscriptions[message["event"]]) + { + sub->on_event(message); + } + } + } else + { + auto handler = response_handlers.front(); + response_handlers.pop(); + auto client = clients.find(handler); + if (client != clients.end()) + { + client->second->handle_response(message); + } + } + } + } catch (const Gio::Error& e) + { + LOGE("IPC error: ", e.what()); + return false; + } + + return true; +} + +void WayfireIPC::subscribe_all(IIPCSubscriber *subscriber) +{ + subscribers.insert(subscriber); + + wf::json_t new_subs; + new_subs["method"] = "window-rules/events/watch"; + send(new_subs.serialize()); +} + +void WayfireIPC::subscribe(IIPCSubscriber *subscriber, const std::vector& events) +{ + wf::json_t new_subs; + new_subs["method"] = "window-rules/events/watch"; + new_subs["events"] = wf::json_t::array(); + + for (auto event : events) + { + if (subscriptions.find(event) == subscriptions.end()) + { + new_subs["events"].append(event); + subscriptions[event] = std::set(); + } + + subscriptions[event].insert(subscriber); + } + + if (new_subs["events"].size() > 0) + { + send(new_subs.serialize()); + } +} + +void WayfireIPC::unsubscribe(IIPCSubscriber *subscriber) +{ + subscribers.erase(subscriber); + + for (auto& [_, subs] : subscriptions) + { + subs.erase(subscriber); + } +} + +std::shared_ptr WayfireIPC::create_client() +{ + auto client = new IPCClient(next_client_id, shared_from_this()); + clients[next_client_id++] = client; + + // Zero is reserved for NO CLIENT id, so just in case :) + if (next_client_id == 0) + { + next_client_id++; + } + + return std::shared_ptr(client); +} + +void WayfireIPC::client_destroyed(int id) +{ + clients.erase(id); +} + +std::shared_ptr WayfireIPC::get_instance() +{ + static std::weak_ptr ipc; + + auto instance = ipc.lock(); + if (!instance) + { + instance = std::shared_ptr(new WayfireIPC()); + ipc = instance; + } + + return instance; +} + +// IPCClient +IPCClient::~IPCClient() +{ + ipc->client_destroyed(id); +} + +void IPCClient::send(const std::string& message) +{ + ipc->send(message); +} + +void IPCClient::send(const std::string& message, response_handler cb) +{ + response_handlers.push(cb); + ipc->send(message, id); +} + +void IPCClient::handle_response(wf::json_t response) +{ + auto handler = response_handlers.front(); + response_handlers.pop(); + handler(response); +} + +void IPCClient::subscribe(IIPCSubscriber *subscriber, const std::vector& events) +{ + ipc->subscribe(subscriber, events); +} + +void IPCClient::subscribe_all(IIPCSubscriber *subscriber) +{ + ipc->subscribe_all(subscriber); +} + +void IPCClient::unsubscribe(IIPCSubscriber *subscriber) +{ + ipc->unsubscribe(subscriber); +} diff --git a/src/util/wf-ipc.hpp b/src/util/wf-ipc.hpp new file mode 100644 index 00000000..968190f4 --- /dev/null +++ b/src/util/wf-ipc.hpp @@ -0,0 +1,84 @@ +#ifndef WF_IPC_HPP +#define WF_IPC_HPP + +#include "giomm/cancellable.h" +#include "giomm/outputstream.h" +#include "giomm/socketconnection.h" +#include "glibmm/iochannel.h" +#include "glibmm/refptr.h" +#include +#include "sigc++/connection.h" +#include +#include +#include +#include +#include +#include +#include + +class IIPCSubscriber +{ + public: + virtual void on_event(wf::json_t) = 0; +}; + +using response_handler = std::function; + +class WayfireIPC; +class IPCClient +{ + int id; + std::shared_ptr ipc; + std::queue response_handlers; + + public: + IPCClient(int id, std::shared_ptr ipc) : id(id), ipc(ipc) + {} + ~IPCClient(); + int get_id(); + void handle_response(wf::json_t response); + void send(const std::string& message); + void send(const std::string& message, response_handler cb); + void subscribe(IIPCSubscriber *subscriber, const std::vector& events); + void subscribe_all(IIPCSubscriber *subscriber); + void unsubscribe(IIPCSubscriber *subscriber); +}; + +class WayfireIPC : public std::enable_shared_from_this +{ + std::queue response_handlers; + std::set subscribers; + std::unordered_map> subscriptions; + int next_client_id{1}; + std::unordered_map clients; + sigc::connection sig_connection; + Glib::RefPtr connection; + Glib::RefPtr input; + Glib::RefPtr output; + Glib::RefPtr cancel; + std::queue write_queue; + bool writing = false; + + void connect(); + void disconnect(); + void send_message(const std::string& message); + bool send_queue(Glib::IOCondition cond); + bool receive(Glib::IOCondition cond); + void write_stream(const std::string& message); + void write_next(); + + public: + void send(const std::string& message); + void send(const std::string& message, int response_handler); + void subscribe(IIPCSubscriber *subscriber, const std::vector& events); + void subscribe_all(IIPCSubscriber *subscriber); + void unsubscribe(IIPCSubscriber *subscriber); + std::shared_ptr create_client(); + void client_destroyed(int id); + + static std::shared_ptr get_instance(); + WayfireIPC(); + ~WayfireIPC(); +}; + +#endif // WF_IPC_HPP diff --git a/src/util/wf-shell-app.hpp b/src/util/wf-shell-app.hpp index 44c1357b..c7036ba8 100644 --- a/src/util/wf-shell-app.hpp +++ b/src/util/wf-shell-app.hpp @@ -1,6 +1,7 @@ #ifndef WF_SHELL_APP_HPP #define WF_SHELL_APP_HPP +#include #include #include @@ -11,6 +12,7 @@ #include "wayfire-shell-unstable-v2-client-protocol.h" using GMonitor = Glib::RefPtr; + /** * Represents a single output */ diff --git a/subprojects/wf-json b/subprojects/wf-json new file mode 160000 index 00000000..830739b5 --- /dev/null +++ b/subprojects/wf-json @@ -0,0 +1 @@ +Subproject commit 830739b5d978019f8d421662fb7b280ba21b5362 diff --git a/wf-shell.ini.example b/wf-shell.ini.example index d381b4cf..5ac56363 100644 --- a/wf-shell.ini.example +++ b/wf-shell.ini.example @@ -18,7 +18,7 @@ randomize = 0 # Special widgets are "spacing" and "separator" widgets, they can be used to add padding everywhere on the panel # To use them, just append the amount of pixels you want as a padding # to the word "spacing" or "separator" and use it as a plugin -widgets_left = menu spacing4 launchers window-list +widgets_left = menu spacing4 language spacing4 launchers window-list widgets_center = none widgets_right = command-output tray notifications volume network battery clock