diff --git a/data/css/default.css b/data/css/default.css index fb09b080..448c6021 100644 --- a/data/css/default.css +++ b/data/css/default.css @@ -59,4 +59,16 @@ .wf-panel .command-output.icon-bottom label { 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; } \ No newline at end of file diff --git a/meson.build b/meson.build index 147c4e35..ed55e61d 100644 --- a/meson.build +++ b/meson.build @@ -22,6 +22,8 @@ gtklayershell = dependency('gtk-layer-shell-0', version: '>= 0.6', fallback: [' libpulse = dependency('libpulse', required : get_option('pulse')) dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4') libgvc = subproject('gvc', default_options: ['static=true'], required : get_option('pulse')) +xkbregistry = dependency('xkbregistry') +json = dependency('nlohmann_json') if get_option('wayland-logout') == true wayland_logout = subproject('wayland-logout') diff --git a/src/panel/meson.build b/src/panel/meson.build index 0a511c76..5f5754d1 100644 --- a/src/panel/meson.build +++ b/src/panel/meson.build @@ -2,6 +2,7 @@ widget_sources = ['widgets/battery.cpp', 'widgets/menu.cpp', 'widgets/clock.cpp', 'widgets/command-output.cpp', + 'widgets/language.cpp', 'widgets/launchers.cpp', 'widgets/network.cpp', 'widgets/spacing.cpp', @@ -17,7 +18,7 @@ widget_sources = ['widgets/battery.cpp', 'widgets/tray/item.cpp', 'widgets/tray/host.cpp'] -deps = [gtkmm, wayland_client, libutil, wf_protos, wfconfig, gtklayershell, dbusmenu_gtk] +deps = [gtkmm, wayland_client, libutil, wf_protos, wfconfig, gtklayershell, dbusmenu_gtk, xkbregistry] if libpulse.found() widget_sources += 'widgets/volume.cpp' diff --git a/src/panel/panel.cpp b/src/panel/panel.cpp index 566dd517..48fdb581 100644 --- a/src/panel/panel.cpp +++ b/src/panel/panel.cpp @@ -6,8 +6,8 @@ #include #include -#include #include +#include #include #include @@ -16,8 +16,10 @@ #include "panel.hpp" #include "../util/gtk-utils.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" @@ -36,6 +38,7 @@ class WayfirePanel::impl { std::unique_ptr window; + WayfireIPC* ipc; Gtk::HBox content_box; Gtk::HBox left_box, center_box, right_box; @@ -226,6 +229,11 @@ class WayfirePanel::impl return Widget(new WfCommandOutputButtons()); } + if (name == "language") + { + return Widget(new WayfireLanguage(ipc)); + } + if (auto pixel = widget_with_value(name, "spacing")) { return Widget(new WayfireSpacing(*pixel)); @@ -313,7 +321,7 @@ class WayfirePanel::impl } public: - impl(WayfireOutput *output) : output(output) + impl(WayfireOutput *output, WayfireIPC *ipc) : ipc(ipc), output(output) { create_window(); } @@ -347,8 +355,9 @@ class WayfirePanel::impl } }; -WayfirePanel::WayfirePanel(WayfireOutput *output) : pimpl(new impl(output)) +WayfirePanel::WayfirePanel(WayfireOutput *output, WayfireIPC *ipc) : pimpl(new impl(output, ipc)) {} + wl_surface*WayfirePanel::get_wl_surface() { return pimpl->get_wl_surface(); @@ -437,7 +446,7 @@ void WayfirePanelApp::add_css_file(std::string file, int priority) void WayfirePanelApp::handle_new_output(WayfireOutput *output) { priv->panels[output] = std::unique_ptr( - new WayfirePanel(output)); + new WayfirePanel(output, ipc)); } WayfirePanel*WayfirePanelApp::panel_for_wl_output(wl_output *output) diff --git a/src/panel/panel.hpp b/src/panel/panel.hpp index c20f94b9..1f658d36 100644 --- a/src/panel/panel.hpp +++ b/src/panel/panel.hpp @@ -6,12 +6,13 @@ #include #include +#include "wf-ipc.hpp" #include "wf-shell-app.hpp" class WayfirePanel { public: - WayfirePanel(WayfireOutput *output); + WayfirePanel(WayfireOutput *output, WayfireIPC *ipc); wl_surface *get_wl_surface(); Gtk::Window& get_window(); diff --git a/src/panel/widgets/language.cpp b/src/panel/widgets/language.cpp new file mode 100644 index 00000000..72e2b9a1 --- /dev/null +++ b/src/panel/widgets/language.cpp @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "language.hpp" +#include "gtkmm/button.h" +#include "sigc++/functors/mem_fun.h" + +void WayfireLanguage::init(Gtk::HBox *container) { + button.get_style_context()->add_class("language"); + button.add(label); + button.get_style_context()->add_class("flat"); + button.get_style_context()->remove_class("activated"); + button.signal_clicked().connect_notify(sigc::mem_fun(this, &WayfireLanguage::next_layout)); + button.show(); + label.show(); + + ipc->subscribe(this, {"keyboard-modifier-state-changed"}); + ipc->send("{\"method\":\"wayfire/get-keyboard-state\"}", [=](nlohmann::json data) { + set_available(data["possible-layouts"]); + set_current(data["layout-index"]); + }); + + container->pack_start(button, false, false); +} + +void WayfireLanguage::on_event(nlohmann::json data) { + if (data["event"] == "keyboard-modifier-state-changed") { + if (available_layouts.size() == 0) { + set_available(data["state"]["possible-layouts"]); + } + + auto state_layout = data["state"]["layout-index"]; + 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; + } + label.set_text(available_layouts[current_layout].ID); + return true; +} + +void WayfireLanguage::set_current(uint32_t index) { + current_layout = index; + update_label(); +} + +void WayfireLanguage::set_available(nlohmann::json 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; + } + + nlohmann::json message; + message["method"] = "wayfire/set-keyboard-state"; + message["data"] = nlohmann::json::object(); + message["data"]["layout-index"] = next; + ipc->send(message.dump()); +} + +WayfireLanguage::WayfireLanguage(WayfireIPC *ipc): ipc(ipc) +{} + +WayfireLanguage::~WayfireLanguage() { + ipc->unsubscribe(this); +} diff --git a/src/panel/widgets/language.hpp b/src/panel/widgets/language.hpp new file mode 100644 index 00000000..825a0def --- /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; + + WayfireIPC *ipc; + uint32_t current_layout; + std::vector available_layouts; + + public: + void init(Gtk::HBox *container) override; + void on_event(nlohmann::json data) override; + bool update_label(); + void set_current(uint32_t index); + void set_available(nlohmann::json layouts); + void next_layout(); + WayfireLanguage(WayfireIPC *ipc); + ~WayfireLanguage(); +}; + +#endif /* end of include guard: WIDGETS_LANGUAGE_HPP */ diff --git a/src/util/meson.build b/src/util/meson.build index 056f79db..855024a2 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -1,5 +1,5 @@ -util = static_library('util', ['gtk-utils.cpp', 'wf-shell-app.cpp', 'wf-autohide-window.cpp', 'wf-popover.cpp'], - dependencies: [wf_protos, wayland_client, gtkmm, wfconfig, libinotify, gtklayershell]) +util = static_library('util', ['gtk-utils.cpp', 'wf-shell-app.cpp', 'wf-autohide-window.cpp', 'wf-popover.cpp', 'wf-ipc.cpp'], + dependencies: [wf_protos, wayland_client, gtkmm, wfconfig, libinotify, gtklayershell, json]) util_includes = include_directories('.') libutil = declare_dependency( diff --git a/src/util/wf-ipc.cpp b/src/util/wf-ipc.cpp new file mode 100644 index 00000000..f96555ec --- /dev/null +++ b/src/util/wf-ipc.cpp @@ -0,0 +1,194 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wf-ipc.hpp" +#include "glibmm/iochannel.h" +#include "glibmm/main.h" +#include "sigc++/functors/mem_fun.h" + +WayfireIPC::WayfireIPC(): socket_fd(-1) +{ + connect(); + + auto connection = Glib::signal_io().connect( + sigc::mem_fun(this, &WayfireIPC::receive), socket_fd, Glib::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"); + } + + socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_fd == -1) + { + throw std::runtime_error("Wayfire socket create error"); + } + + if (fcntl(socket_fd, F_SETFL, O_NONBLOCK) == -1) + { + throw std::runtime_error("Wayfire socket set flag error"); + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + + if (strlen(socket_path) >= sizeof(addr.sun_path)) + { + throw std::runtime_error("Wayfire socket path too long"); + } + + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + if (::connect(socket_fd, (sockaddr*)&addr, sizeof(addr)) == -1) + { + throw std::runtime_error("Wayfire socket connect error"); + } +} + +void WayfireIPC::disconnect() +{ + if (socket_fd != -1) + { + close(socket_fd); + socket_fd = -1; + } +} + +void WayfireIPC::send(const std::string& message) +{ + send_message(message); + response_handlers.push(std::nullopt); +} + +void WayfireIPC::send(const std::string& message, response_handler cb) +{ + send_message(message); + response_handlers.push(cb); +} + +void WayfireIPC::send_message(const std::string& message) +{ + size_t sent = 0; + while (sent < message.size()) { + uint32_t length = message.size(); + auto all_data = std::string((char*)&length, 4); + all_data += message; + ssize_t res = ::send(socket_fd, all_data.data() + sent, + all_data.size() - sent, MSG_NOSIGNAL); + if (res == -1) { + throw std::system_error(errno, std::system_category(), "send failed"); + } + + sent += res; + } +} + +bool WayfireIPC::receive(Glib::IOCondition cond) +{ + ssize_t received = 0; + + if (!length_received) + { + received = ::recv(socket_fd, &length, 4, 0); + if (received == -1) { + return true; + } + if (received == 0) { + throw std::runtime_error("Connection closed by peer"); + } + } + length_received = true; + + std::string buf(length + 1, 0); + received = ::recv(socket_fd, &buf[0], length, 0); + if (received == -1) { + return true; + } + if (received == 0) { + throw std::runtime_error("Connection closed by peer"); + } + length_received = false; + + auto message = nlohmann::json::parse(buf); + + if (message.contains("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(); + if (handler.has_value()) { + handler.value()(message); + } + } + + return true; +} + +void WayfireIPC::subscribe_all(IIPCSubscriber* subscriber) +{ + subscribers.insert(subscriber); + + nlohmann::json new_subs; + new_subs["method"] = "window-rules/events/watch"; + send(new_subs.dump()); +} + +void WayfireIPC::subscribe(IIPCSubscriber* subscriber, const std::vector& events) +{ + nlohmann::json new_subs; + new_subs["method"] = "window-rules/events/watch"; + new_subs["events"] = nlohmann::json::array(); + + for (auto event : events) { + if (subscriptions.find(event) == subscriptions.end()) { + new_subs["events"].push_back(event); + subscriptions[event] = std::set(); + } + + subscriptions[event].insert(subscriber); + } + + if (new_subs["events"].size() > 0) { + send(new_subs.dump()); + } +} + +void WayfireIPC::unsubscribe(IIPCSubscriber* subscriber) +{ + subscribers.erase(subscriber); + + for (auto& [_, subs] : subscriptions) { + subs.erase(subscriber); + } +} + diff --git a/src/util/wf-ipc.hpp b/src/util/wf-ipc.hpp new file mode 100644 index 00000000..4580db38 --- /dev/null +++ b/src/util/wf-ipc.hpp @@ -0,0 +1,45 @@ +#ifndef WF_IPC_HPP +#define WF_IPC_HPP + +#include "glibmm/iochannel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class IIPCSubscriber { + public: + virtual void on_event(nlohmann::json) = 0; +}; + +using response_handler = std::function; + +class WayfireIPC +{ + int socket_fd; + std::queue> response_handlers; + std::set subscribers; + std::unordered_map> subscriptions; + bool length_received = false; + uint32_t length; + + void connect(); + void disconnect(); + void send_message(const std::string& message); + bool receive(Glib::IOCondition cond); + public: + void send(const std::string& message); + void send(const std::string& message, response_handler); + void subscribe(IIPCSubscriber* subscriber, const std::vector& events); + void subscribe_all(IIPCSubscriber* subscriber); + void unsubscribe(IIPCSubscriber* subscriber); + WayfireIPC(); + ~WayfireIPC(); +}; + +#endif // WF_IPC_HPP \ No newline at end of file diff --git a/src/util/wf-shell-app.cpp b/src/util/wf-shell-app.cpp index 7eee3f0a..cdd96b3c 100644 --- a/src/util/wf-shell-app.cpp +++ b/src/util/wf-shell-app.cpp @@ -1,4 +1,5 @@ #include "wf-shell-app.hpp" +#include "wf-ipc.hpp" #include #include #include @@ -154,6 +155,8 @@ void WayfireShellApp::on_activate() std::exit(-1); } + ipc = new WayfireIPC(); + wl_registry *registry = wl_display_get_registry(wl_display); wl_registry_add_listener(registry, ®istry_listener, this); wl_display_roundtrip(wl_display); @@ -232,7 +235,9 @@ WayfireShellApp::WayfireShellApp(int argc, char **argv) } WayfireShellApp::~WayfireShellApp() -{} +{ + delete ipc; +} std::unique_ptr WayfireShellApp::instance; WayfireShellApp& WayfireShellApp::get() @@ -273,4 +278,4 @@ WayfireOutput::~WayfireOutput() sigc::signal WayfireOutput::toggle_menu_signal() { return m_toggle_menu_signal; -} +} \ No newline at end of file diff --git a/src/util/wf-shell-app.hpp b/src/util/wf-shell-app.hpp index 90f2f2b1..2c4fe94d 100644 --- a/src/util/wf-shell-app.hpp +++ b/src/util/wf-shell-app.hpp @@ -1,7 +1,6 @@ #ifndef WF_SHELL_APP_HPP #define WF_SHELL_APP_HPP -#include #include #include @@ -9,8 +8,10 @@ #include #include "wayfire-shell-unstable-v2-client-protocol.h" +#include "wf-ipc.hpp" using GMonitor = Glib::RefPtr; + /** * Represents a single output */ @@ -66,6 +67,7 @@ class WayfireShellApp int inotify_css_fd; wf::config::config_manager_t config; zwf_shell_manager_v2 *wf_shell_manager = nullptr; + WayfireIPC *ipc = nullptr; WayfireShellApp(int argc, char **argv); virtual ~WayfireShellApp(); 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