summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@opera.com>2026-01-12 23:06:20 +0100
committerJoel Klinghed <the_jk@opera.com>2026-01-12 23:06:20 +0100
commitdfeb19b0a83b8ce57d28bf94a4f8d129993d1064 (patch)
treed352908df286058059e306c350d89a07c67049eb /src
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/args.cc389
-rw-r--r--src/args.hh64
-rw-r--r--src/dbus_common.cc7
-rw-r--r--src/dbus_common.hh14
-rw-r--r--src/find_desktop.cc152
-rw-r--r--src/find_desktop.hh10
-rw-r--r--src/main.cc59
-rw-r--r--src/monitor.cc626
-rw-r--r--src/monitor.hh13
-rw-r--r--src/notify.cc59
-rw-r--r--src/notify.hh12
-rw-r--r--src/xcb_atoms.cc81
-rw-r--r--src/xcb_atoms.hh57
-rw-r--r--src/xcb_colors.cc96
-rw-r--r--src/xcb_colors.hh57
-rw-r--r--src/xcb_connection.cc25
-rw-r--r--src/xcb_connection.hh28
-rw-r--r--src/xcb_event.hh30
-rw-r--r--src/xcb_resource.cc41
-rw-r--r--src/xcb_resource.hh99
-rw-r--r--src/xcb_resources.hh35
-rw-r--r--src/xcb_resources_none.cc37
-rw-r--r--src/xcb_resources_xrm.cc74
-rw-r--r--src/xcb_xkb.cc167
-rw-r--r--src/xcb_xkb.hh30
25 files changed, 2262 insertions, 0 deletions
diff --git a/src/args.cc b/src/args.cc
new file mode 100644
index 0000000..1794941
--- /dev/null
+++ b/src/args.cc
@@ -0,0 +1,389 @@
+#include "args.hh"
+
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <format>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+std::string kEmpty;
+
+class OptionImpl : public Args::OptionArgument {
+ public:
+ enum Type : uint8_t {
+ NoArgument,
+ RequiredArgument,
+ OptionalArgument,
+ };
+
+ OptionImpl(Type type, char shortname, std::string longname, std::string arg,
+ std::string help)
+ : type(type),
+ shortname(shortname),
+ longname(std::move(longname)),
+ arg(std::move(arg)),
+ help(std::move(help)) {}
+
+ const Type type;
+ const char shortname;
+ const std::string longname;
+ const std::string arg;
+ const std::string help;
+
+ [[nodiscard]] bool is_set() const override { return is_set_; }
+
+ [[nodiscard]] bool has_argument() const override {
+ return value_.has_value();
+ }
+
+ [[nodiscard]] const std::string& argument() const override {
+ if (value_.has_value())
+ return value_.value();
+ assert(false);
+ return kEmpty;
+ }
+
+ void clear() {
+ is_set_ = false;
+ value_.reset();
+ }
+
+ void set_argument(std::string value) {
+ assert(type == Type::RequiredArgument || type == Type::OptionalArgument);
+ is_set_ = true;
+ value_ = std::move(value);
+ }
+
+ void set_no_argument() {
+ assert(type == Type::NoArgument || type == Type::OptionalArgument);
+ is_set_ = true;
+ value_.reset();
+ }
+
+ private:
+ bool is_set_{false};
+ std::optional<std::string> value_;
+};
+
+class ArgsImpl : public Args {
+ public:
+ explicit ArgsImpl(std::string prgname) : prgname_(std::move(prgname)) {}
+
+ std::shared_ptr<Option> option(char shortname, std::string longname,
+ std::string help) override {
+ auto opt = std::make_shared<OptionImpl>(
+ OptionImpl::Type::NoArgument, shortname, std::move(longname),
+ /* arg */ std::string(), std::move(help));
+ add(opt);
+ return opt;
+ }
+
+ std::shared_ptr<OptionArgument> option_argument(char shortname,
+ std::string longname,
+ std::string arg,
+ std::string help,
+ bool required) override {
+ auto opt = std::make_shared<OptionImpl>(
+ required ? OptionImpl::Type::RequiredArgument
+ : OptionImpl::Type::OptionalArgument,
+ shortname, std::move(longname), std::move(arg), std::move(help));
+ add(opt);
+ return opt;
+ }
+
+ bool run(int argc, char** argv,
+ std::vector<std::string_view>* arguments = nullptr) override {
+ last_error_.clear();
+ for (auto& opt : options_) {
+ opt->clear();
+ }
+
+ std::string_view prgname;
+ if (prgname_.empty()) {
+ if (argc > 0)
+ prgname = argv[0];
+ } else {
+ prgname = prgname_;
+ }
+
+ for (int a = 1; a < argc; ++a) {
+ assert(argv[a]);
+ if (argv[a][0] == '-' && argv[a][1] != '\0') {
+ if (argv[a][1] == '-') {
+ // long option
+ size_t eq = 2;
+ while (argv[a][eq] != '=' && argv[a][eq] != '\0')
+ ++eq;
+ size_t end = eq;
+ while (argv[a][end] != '\0')
+ ++end;
+
+ if (end == 2) {
+ // "--", no more options signal
+ if (arguments) {
+ for (++a; a < argc; ++a)
+ arguments->emplace_back(argv[a]);
+ }
+ break;
+ }
+
+ auto name = std::string_view(argv[a] + 2, eq - 2);
+ auto it = long_.find(name);
+ if (it == long_.end()) {
+ last_error_ =
+ std::format("{}: unrecognized option '--{}'", prgname, name);
+ return false;
+ }
+ auto& opt = options_[it->second];
+
+ if (eq < end) {
+ // long option with argument after equal sign
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ last_error_ =
+ std::format("{}: option '--{}' doesn't allow an argument",
+ prgname, name);
+ return false;
+ case OptionImpl::Type::RequiredArgument:
+ case OptionImpl::Type::OptionalArgument:
+ opt->set_argument(
+ std::string(argv[a] + eq + 1, end - (eq + 1)));
+ break;
+ }
+ } else {
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ opt->set_no_argument();
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ if (++a >= argc) {
+ last_error_ = std::format(
+ "{}: option '--{}' requires an argument", prgname, name);
+ return false;
+ }
+ opt->set_argument(argv[a]);
+ break;
+ }
+ }
+ } else {
+ // short options
+ char* current = argv[a] + 1;
+ for (; *current; ++current) {
+ auto it = short_.find(*current);
+ if (it == short_.end()) {
+ last_error_ =
+ std::format("{}: invalid option -- '{}'", prgname, *current);
+ return false;
+ }
+
+ auto& opt = options_[it->second];
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ opt->set_no_argument();
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ if (++a >= argc) {
+ last_error_ =
+ std::format("{}: option requires an argument -- '{}'",
+ prgname, *current);
+ return false;
+ }
+ opt->set_argument(argv[a]);
+ break;
+ }
+ }
+ }
+ } else {
+ if (arguments)
+ arguments->emplace_back(argv[a]);
+ }
+ }
+ return true;
+ }
+
+ void print_error(std::ostream& out) const override {
+ if (last_error_.empty())
+ return;
+
+ out << last_error_ << '\n';
+ }
+
+ void print_help(std::ostream& out, uint32_t width = 79) const override {
+ if (options_.empty())
+ return;
+
+ uint32_t indent = 0;
+ const uint32_t max_need = width / 2;
+ std::vector<uint32_t> option_need;
+ for (auto const& opt : options_) {
+ uint32_t need;
+ if (opt->longname.empty()) {
+ need = 4; // -O
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ need += 1 + (opt->arg.empty() ? 3 : opt->arg.size());
+ break;
+ }
+ } else {
+ need = 8 + opt->longname.size(); // -O, --option
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ // =ARG
+ need += 1 + (opt->arg.empty() ? 3 : opt->arg.size());
+ break;
+ case OptionImpl::Type::OptionalArgument:
+ // [=ARG]
+ need += 3 + (opt->arg.empty() ? 3 : opt->arg.size());
+ break;
+ }
+ }
+ need += 2; // margin
+
+ option_need.emplace_back(need);
+ if (need <= max_need) {
+ indent = std::max(indent, need);
+ }
+ }
+
+ print_wrap(out, width, /* indent */ 0,
+ "Mandatory arguments to long options"
+ " are mandatory for short options too.");
+ auto need_it = option_need.begin();
+ for (auto const& opt : options_) {
+ if (opt->longname.empty()) {
+ out << " -" << opt->shortname;
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ out << " " << (opt->arg.empty() ? "ARG" : opt->arg);
+ break;
+ }
+ } else {
+ if (opt->shortname != '\0') {
+ out << " -" << opt->shortname << ", --";
+ } else {
+ out << " --";
+ }
+ out << opt->longname;
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ out << "=" << (opt->arg.empty() ? "ARG" : opt->arg);
+ break;
+ case OptionImpl::Type::OptionalArgument:
+ out << "=[" << (opt->arg.empty() ? "ARG" : opt->arg) << ']';
+ break;
+ }
+ }
+
+ auto need = *need_it++;
+ if (need > max_need) {
+ out << '\n';
+ if (!opt->help.empty()) {
+ print_wrap(out, width, 0, opt->help);
+ }
+ } else {
+ if (opt->help.empty()) {
+ out << '\n';
+ } else {
+ out << " "; // add margin, already included in need
+ while (need++ < indent)
+ out << ' ';
+ print_wrap(out, width, indent, opt->help);
+ }
+ }
+ }
+ }
+
+ private:
+ void add(std::shared_ptr<OptionImpl> opt) {
+ if (opt->shortname == '\0' && opt->longname.empty()) {
+ assert(false);
+ } else {
+ auto idx = options_.size();
+ if (opt->shortname != '\0')
+ short_.emplace(opt->shortname, idx);
+ if (!opt->longname.empty())
+ long_.emplace(opt->longname, idx);
+ }
+ options_.emplace_back(std::move(opt));
+ }
+
+ static inline bool is_whitespace(char c) { return c == ' ' || c == '\t'; }
+
+ static void print_wrap(std::ostream& out, uint32_t width, uint32_t indent,
+ const std::string& str) {
+ if (indent + str.size() <= width) {
+ out << str << '\n';
+ return;
+ }
+ if (width <= indent || indent + 10 > width) {
+ out << '\n';
+ out << str << '\n';
+ return;
+ }
+ const std::string indent_str(indent, ' ');
+ const uint32_t avail = width - indent;
+ size_t offset = 0;
+ while (offset + avail < str.size()) {
+ uint32_t i = avail;
+ while (i > 0 && !is_whitespace(str[offset + i]))
+ --i;
+ if (i == 0) {
+ out << str.substr(offset, avail - 1);
+ out << "-\n";
+ offset += avail - 1;
+ } else {
+ out << str.substr(offset, i);
+ out << '\n';
+ offset += i;
+ }
+ out << indent_str;
+ }
+ out << str.substr(offset);
+ out << '\n';
+ }
+
+ const std::string prgname_;
+ std::vector<std::shared_ptr<OptionImpl>> options_;
+ std::map<char, size_t> short_;
+ std::map<std::string_view, size_t> long_;
+ std::string last_error_;
+};
+
+} // namespace
+
+std::shared_ptr<Args::Option> Args::option(std::string longname,
+ std::string help) {
+ return option(/* shortname */ '\0', std::move(longname), std::move(help));
+}
+
+std::shared_ptr<Args::OptionArgument> Args::option_argument(
+ std::string longname, std::string arg, std::string help, bool required) {
+ return option_argument(/* shortname */ '\0', std::move(longname),
+ std::move(arg), std::move(help), required);
+}
+
+std::unique_ptr<Args> Args::create(std::string prgname) {
+ return std::make_unique<ArgsImpl>(std::move(prgname));
+}
diff --git a/src/args.hh b/src/args.hh
new file mode 100644
index 0000000..14f3716
--- /dev/null
+++ b/src/args.hh
@@ -0,0 +1,64 @@
+#ifndef ARGS_HH
+#define ARGS_HH
+
+#include <cstdint>
+#include <iosfwd>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+class Args {
+ public:
+ virtual ~Args() = default;
+
+ class Option {
+ public:
+ virtual ~Option() = default;
+
+ [[nodiscard]] virtual bool is_set() const = 0;
+
+ protected:
+ Option() = default;
+ Option(Option const&) = delete;
+ Option& operator=(Option const&) = delete;
+ };
+
+ class OptionArgument : public Option {
+ public:
+ [[nodiscard]] virtual bool has_argument() const = 0;
+ [[nodiscard]] virtual const std::string& argument() const = 0;
+ };
+
+ static std::unique_ptr<Args> create(std::string prgname = std::string());
+
+ virtual std::shared_ptr<Option> option(char shortname,
+ std::string longname = std::string(),
+ std::string help = std::string()) = 0;
+
+ std::shared_ptr<Option> option(std::string longname,
+ std::string help = std::string());
+
+ virtual std::shared_ptr<OptionArgument> option_argument(
+ char shortname, std::string longname = std::string(),
+ std::string arg = std::string(), std::string help = std::string(),
+ bool required = true) = 0;
+
+ std::shared_ptr<OptionArgument> option_argument(
+ std::string longname, std::string arg = std::string(),
+ std::string help = std::string(), bool required = true);
+
+ virtual bool run(int argc, char** argv,
+ std::vector<std::string_view>* arguments = nullptr) = 0;
+
+ virtual void print_error(std::ostream& out) const = 0;
+
+ virtual void print_help(std::ostream& out, uint32_t width = 79) const = 0;
+
+ protected:
+ Args() = default;
+ Args(Args const&) = delete;
+ Args& operator=(Args const&) = delete;
+};
+
+#endif // ARGS_HH
diff --git a/src/dbus_common.cc b/src/dbus_common.cc
new file mode 100644
index 0000000..c3f7f2b
--- /dev/null
+++ b/src/dbus_common.cc
@@ -0,0 +1,7 @@
+#include "dbus_common.hh"
+
+#include <sdbus-c++/sdbus-c++.h>
+
+const sdbus::ServiceName dbus::kServiceName{"org.the_jk.claudemon"};
+const sdbus::ObjectPath dbus::kObjectPath{"/org/the_jk/claudemon/notify"};
+const sdbus::InterfaceName dbus::kInterfaceName{"org.the_jk.claudemon.Notify"};
diff --git a/src/dbus_common.hh b/src/dbus_common.hh
new file mode 100644
index 0000000..b4f7501
--- /dev/null
+++ b/src/dbus_common.hh
@@ -0,0 +1,14 @@
+#ifndef DBUS_COMMON_HH
+#define DBUS_COMMON_HH
+
+#include <sdbus-c++/sdbus-c++.h>
+
+namespace dbus {
+
+extern const sdbus::ServiceName kServiceName;
+extern const sdbus::ObjectPath kObjectPath;
+extern const sdbus::InterfaceName kInterfaceName;
+
+} // namespace dbus
+
+#endif // DBUS_COMMON_HH
diff --git a/src/find_desktop.cc b/src/find_desktop.cc
new file mode 100644
index 0000000..a3a9330
--- /dev/null
+++ b/src/find_desktop.cc
@@ -0,0 +1,152 @@
+#include "find_desktop.hh"
+
+#include "xcb_atoms.hh"
+#include "xcb_connection.hh"
+#include "xcb_event.hh"
+#include "xcb_resource.hh"
+
+#include <cassert>
+#include <charconv>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <ctime>
+#include <optional>
+#include <string>
+#include <system_error>
+#include <xcb/xproto.h>
+
+namespace {
+
+int get_from_e17_using_window(xcb_connection_t* conn, xcb_window_t wnd,
+ xcb_atom_t e_window_desk) {
+ if (e_window_desk == XCB_ATOM_NONE) {
+ return -1;
+ }
+
+ for (int i = 0; i < 2; ++i) {
+ auto cookie =
+ xcb_get_property(conn, 0, wnd, e_window_desk, XCB_ATOM_CARDINAL, 0, 2);
+ xcb::reply<xcb_get_property_reply_t> reply(
+ xcb_get_property_reply(conn, cookie, nullptr));
+
+ if (reply && reply->format == 32 &&
+ xcb_get_property_value_length(reply.get()) >= 8) {
+ auto* data = static_cast<uint32_t*>(xcb_get_property_value(reply.get()));
+ auto x = data[0];
+ auto y = data[1];
+ return static_cast<int>((x * 256) + y);
+ }
+
+ xcb_flush(conn);
+ struct timespec ts = {.tv_sec = 0, .tv_nsec = 250000000}; // 250ms
+ nanosleep(&ts, nullptr);
+ }
+ return -1;
+}
+
+std::string create_desktop(std::string const& prefix, int desktop,
+ bool use_prefix) {
+ std::string ret;
+ if (use_prefix) {
+ ret.append(prefix);
+ ret.push_back('_');
+ }
+ auto const offset = ret.size();
+ ret.resize(offset + 10);
+ auto [ptr, ec] =
+ std::to_chars(ret.data() + offset, ret.data() + ret.size(), desktop);
+ if (ec == std::errc()) {
+ ret.resize(ptr - ret.data());
+ } else {
+ assert(false);
+ ret.resize(offset);
+ }
+ return ret;
+}
+
+} // namespace
+
+std::optional<std::string> find_desktop(std::optional<std::string> display,
+ bool use_prefix) {
+ int screen_num = 0;
+ auto conn = xcb::make_shared_conn(xcb_connect(
+ display.has_value() ? display.value().c_str() : nullptr, &screen_num));
+ if (!conn || xcb_connection_has_error(conn.get())) {
+ return std::nullopt;
+ }
+
+ auto atoms = xcb::Atoms::create(conn);
+ if (!atoms) {
+ return std::nullopt;
+ }
+
+ auto* screen = xcb::get_screen(conn.get(), screen_num);
+ if (!screen) {
+ return std::nullopt;
+ }
+
+ xcb_window_t root = screen->root;
+
+ auto net_current_desktop_atom = atoms->get("_NET_CURRENT_DESKTOP");
+ auto e_window_desk_atom = atoms->get("__E_WINDOW_DESK");
+
+ if (!atoms->sync()) {
+ return std::nullopt;
+ }
+
+ auto* desktop_env = getenv("DESKTOP");
+ if (!desktop_env || strncmp(desktop_env, "Enlightenment-0.17", 18) != 0) {
+ /* Skip _NET_CURRENT_DESKTOP if we're in e17 */
+ /* Try to read the _NET_CURRENT_DESKTOP property */
+ auto net_current_desktop = net_current_desktop_atom.get();
+ if (net_current_desktop != XCB_ATOM_NONE) {
+ auto cookie = xcb_get_property(conn.get(), 0, root, net_current_desktop,
+ XCB_ATOM_CARDINAL, 0, 1);
+ xcb::reply<xcb_get_property_reply_t> reply(
+ xcb_get_property_reply(conn.get(), cookie, nullptr));
+
+ if (reply && reply->format == 32 &&
+ xcb_get_property_value_length(reply.get()) >= 4) {
+ auto* data =
+ static_cast<uint32_t*>(xcb_get_property_value(reply.get()));
+ return create_desktop("ewmh", static_cast<int>(data[0]), use_prefix);
+ }
+ }
+ }
+
+ /* Failing that, let's see if our owner knows */
+ auto* windowid_env = getenv("WINDOWID");
+ if (windowid_env) {
+ auto* const end = windowid_env + strlen(windowid_env);
+ unsigned long windowid;
+ auto [ptr, ec] = std::from_chars(windowid_env, end, windowid);
+ if (ptr == end && ec == std::errc{}) {
+ auto ret = get_from_e17_using_window(conn.get(),
+ static_cast<xcb_window_t>(windowid),
+ e_window_desk_atom.get());
+ if (ret != -1) {
+ return create_desktop("enlightenment", ret, use_prefix);
+ }
+ }
+ }
+
+ /* Try creating a bogus window then */
+ {
+ auto wnd = xcb::make_unique_wnd(conn);
+ xcb_create_window(conn.get(), XCB_COPY_FROM_PARENT, wnd->id(), root, 0, 0,
+ 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
+ XCB_COPY_FROM_PARENT, 0, nullptr);
+ xcb_map_window(conn.get(), wnd->id());
+ xcb_flush(conn.get());
+
+ auto ret = get_from_e17_using_window(conn.get(), wnd->id(),
+ e_window_desk_atom.get());
+ if (ret != -1) {
+ return create_desktop("enlightenment", ret, use_prefix);
+ }
+ }
+
+ /* Don't know what else to do */
+ return std::nullopt;
+}
diff --git a/src/find_desktop.hh b/src/find_desktop.hh
new file mode 100644
index 0000000..f066d68
--- /dev/null
+++ b/src/find_desktop.hh
@@ -0,0 +1,10 @@
+#ifndef FIND_DESKTOP_HH
+#define FIND_DESKTOP_HH
+
+#include <optional>
+#include <string>
+
+std::optional<std::string> find_desktop(std::optional<std::string> display,
+ bool use_prefix);
+
+#endif // FIND_DESKTOP_HH
diff --git a/src/main.cc b/src/main.cc
new file mode 100644
index 0000000..c37ee6a
--- /dev/null
+++ b/src/main.cc
@@ -0,0 +1,59 @@
+#include "args.hh"
+#include "monitor.hh"
+#include "notify.hh"
+
+#include <iostream>
+#include <optional>
+#include <string_view>
+#include <vector>
+
+#ifndef VERSION
+# define VERSION ""
+#endif
+
+int main(int argc, char** argv) {
+ auto args = Args::create();
+ auto help = args->option('h', "help", "display this text and exit.");
+ auto version = args->option('V', "version", "display version and exit.");
+ auto opt_font_name = args->option_argument(
+ 'F', "font", "use font named FONT instead of default or Xresources.",
+ "FONT");
+ auto opt_display = args->option_argument(
+ 'D', "display", "connect to DISPLAY instead of default.", "DISPLAY");
+ auto notify = args->option('N', "notification", "claude notification .");
+ std::vector<std::string_view> arguments;
+ if (!args->run(argc, argv, &arguments)) {
+ args->print_error(std::cerr);
+ std::cerr << "Try `claudemon --help` for usage.\n";
+ return EXIT_FAILURE;
+ }
+ if (help->is_set()) {
+ std::cout << "Usage: `claudemon [OPTIONS]`\n"
+ << "Monitors claude.\n"
+ << "\n";
+ args->print_help(std::cout);
+ return EXIT_SUCCESS;
+ }
+ if (version->is_set()) {
+ std::cout << "Timer " VERSION " written by "
+ << "Joel Klinghed <the_jk@spawned.biz>\n";
+ return EXIT_SUCCESS;
+ }
+ if (!arguments.empty()) {
+ std::cerr << "Unexpected arguments after options.\n"
+ << "Try `claudemon --help` for usage.\n";
+ return EXIT_FAILURE;
+ }
+
+ if (notify->is_set()) {
+ return Notify::run(opt_display->has_argument()
+ ? opt_display->argument()
+ : std::optional<std::string>());
+ }
+
+ return Monitor::run(
+ opt_display->has_argument() ? opt_display->argument()
+ : std::optional<std::string>(),
+ opt_font_name->has_argument() ? opt_font_name->argument()
+ : std::optional<std::string>());
+}
diff --git a/src/monitor.cc b/src/monitor.cc
new file mode 100644
index 0000000..d19db4c
--- /dev/null
+++ b/src/monitor.cc
@@ -0,0 +1,626 @@
+#include "monitor.hh"
+
+#include "dbus_common.hh"
+#include "xcb_atoms.hh"
+#include "xcb_colors.hh"
+#include "xcb_connection.hh"
+#include "xcb_event.hh"
+#include "xcb_resource.hh"
+#include "xcb_resources.hh"
+#include "xcb_xkb.hh"
+
+#include <algorithm>
+#include <cassert>
+#include <condition_variable>
+#include <cstddef>
+#include <cstdint>
+#include <deque>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <sdbus-c++/sdbus-c++.h>
+#include <string>
+#include <thread>
+#include <utility>
+#include <vector>
+#include <xcb/xcb_event.h>
+#include <xcb/xcb_icccm.h>
+#include <xcb/xproto.h>
+
+namespace {
+
+constexpr char const kTitle[] = "Claude Code Monitor";
+constexpr char const kClass[] = "org.the_jk.claudemon";
+
+class Server {
+ public:
+ class Delegate {
+ public:
+ virtual ~Delegate() = default;
+
+ virtual void notify(std::string const& session_id,
+ std::string const& event_name,
+ std::optional<std::string> const& cwd,
+ std::optional<std::string> const& notification_type,
+ std::optional<std::string> const& desktop) = 0;
+
+ protected:
+ Delegate() = default;
+ };
+
+ explicit Server(Delegate* delegate) : delegate_(delegate) {}
+
+ virtual ~Server() {
+ if (conn_)
+ conn_->leaveEventLoop();
+ thread_.join();
+ }
+
+ bool init() {
+ try {
+ conn_ = sdbus::createBusConnection(dbus::kServiceName);
+ object_ = sdbus::createObject(*conn_, dbus::kObjectPath);
+
+ object_
+ ->addVTable(sdbus::registerMethod("notify").implementedAs(
+ [this](std::string const& session_id,
+ std::string const& event_name, std::string const& cwd,
+ std::string const& notification_type,
+ std::string const& desktop) {
+ delegate_->notify(
+ session_id, event_name,
+ cwd.empty() ? std::optional<std::string>() : cwd,
+ notification_type.empty() ? std::optional<std::string>()
+ : notification_type,
+ desktop.empty() ? std::optional<std::string>() : desktop);
+ }))
+ .forInterface(dbus::kInterfaceName);
+
+ thread_ = std::thread(&Server::run, this);
+
+ return true;
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to init server: " << err.what() << '\n';
+ return false;
+ }
+ }
+
+ private:
+ void run() { conn_->enterEventLoop(); }
+
+ Delegate* const delegate_;
+ std::shared_ptr<sdbus::IConnection> conn_;
+ std::unique_ptr<sdbus::IObject> object_;
+ std::thread thread_;
+};
+
+class Ui : public Server::Delegate {
+ enum class State : uint8_t {
+ IDLE,
+ PROMPT,
+ BUSY,
+ };
+
+ struct Session {
+ State state{State::IDLE};
+ std::string cwd{"/"};
+ std::string desktop{"0"};
+ std::string name;
+ };
+
+ public:
+ Ui() : server_(std::make_unique<Server>(this)) {}
+
+ bool init() { return server_->init(); }
+
+ int run(std::optional<std::string> display,
+ std::optional<std::string> font_name) {
+ int screen_index = 0;
+ conn_ = xcb::make_shared_conn(
+ xcb_connect(display.has_value() ? display.value().c_str() : nullptr,
+ &screen_index));
+
+ {
+ auto err = xcb_connection_has_error(conn_.get());
+ if (err) {
+ std::cerr << "Unable to connect to X display: " << err << '\n';
+ return EXIT_FAILURE;
+ }
+ }
+
+ auto atoms = xcb::Atoms::create(conn_);
+ string_atom_ = atoms->get("STRING");
+ wm_protocols_ = atoms->get("WM_PROTOCOLS");
+ wm_delete_window_ = atoms->get("WM_DELETE_WINDOW");
+
+ auto* screen = xcb::get_screen(conn_.get(), screen_index);
+ assert(screen);
+
+ auto colors = xcb::Colors::create(conn_, screen->default_colormap);
+ background_ =
+ colors->get_with_fallback(0x00, 0x00, 0x00, screen->black_pixel);
+ foreground_busy_ =
+ colors->get_with_fallback(0x96, 0x96, 0x96, screen->white_pixel);
+ foreground_prompt_ =
+ colors->get_with_fallback(0x96, 0x00, 0x00, screen->white_pixel);
+ foreground_idle_ =
+ colors->get_with_fallback(0x00, 0x96, 0x00, screen->white_pixel);
+
+ keyboard_ = xcb::Keyboard::create(conn_.get());
+ if (!keyboard_) {
+ std::cerr << "Failed to initialize XKB.\n";
+ return EXIT_FAILURE;
+ }
+
+ wnd_ = xcb::make_unique_wnd(conn_);
+ gc_ = xcb::make_unique_gc(conn_);
+ font_ = xcb::make_unique_font(conn_);
+
+ if (!font_name.has_value()) {
+ auto resources = xcb::Resources::create(conn_);
+ font_name = resources->get_string("claudemon.font", "");
+ if (!font_name.has_value()) {
+ font_name = "fixed";
+ }
+ }
+
+ auto font_cookie = xcb_open_font_checked(
+ conn_.get(), font_->id(), font_name->size(), font_name->data());
+
+ if (!atoms->sync()) {
+ std::cerr << "Failed to get X atoms.\n";
+ return EXIT_FAILURE;
+ }
+ if (!colors->sync()) {
+ std::cerr << "Failed to get X colors.\n";
+ return EXIT_FAILURE;
+ }
+
+ uint32_t value_list[3];
+ uint32_t value_mask = 0;
+ value_mask |= XCB_CW_BACK_PIXEL;
+ value_list[0] = screen->black_pixel;
+ value_mask |= XCB_CW_EVENT_MASK;
+ value_list[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS |
+ XCB_EVENT_MASK_BUTTON_PRESS |
+ XCB_EVENT_MASK_STRUCTURE_NOTIFY;
+ xcb_create_window(conn_.get(), XCB_COPY_FROM_PARENT, wnd_->id(),
+ screen->root, 0, 0, wnd_width_, wnd_height_, 0,
+ XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual,
+ value_mask, value_list);
+
+ xcb_icccm_set_wm_name(conn_.get(), wnd_->id(), string_atom_->get(), 8,
+ sizeof(kTitle) - 1, kTitle);
+ xcb_icccm_set_wm_class(conn_.get(), wnd_->id(), sizeof(kClass) - 1, kClass);
+ xcb_atom_t atom_list[1];
+ atom_list[0] = wm_delete_window_->get();
+ xcb_icccm_set_wm_protocols(conn_.get(), wnd_->id(), wm_protocols_->get(), 1,
+ atom_list);
+
+ value_mask = XCB_GC_FONT;
+ value_list[0] = font_->id();
+ xcb_create_gc(conn_.get(), gc_->id(), wnd_->id(), value_mask, value_list);
+
+ {
+ xcb::generic_error error(xcb_request_check(conn_.get(), font_cookie));
+ if (error) {
+ std::cerr << "Failed to load font " << *font_name << ": "
+ << static_cast<int>(error->error_code) << '\n';
+ return EXIT_FAILURE;
+ }
+ }
+
+ update_lines();
+
+ xcb_map_window(conn_.get(), wnd_->id());
+ xcb_flush(conn_.get());
+
+ std::thread xcb_thread(&Ui::run_xcb, this);
+
+ bool quit = false;
+ while (!quit) {
+ ActionData action;
+ {
+ std::unique_lock<std::mutex> lock(action_lock_);
+ while (action_.empty()) {
+ action_cond_.wait(lock);
+ }
+ action = action_.front();
+ action_.pop_front();
+ }
+
+ switch (action.action) {
+ case Action::QUIT:
+ quit = true;
+ break;
+ case Action::DRAW:
+ if (action.last) {
+ draw();
+ }
+ break;
+ case Action::RESIZE:
+ wnd_width_ = action.rect.width;
+ wnd_height_ = action.rect.height;
+ break;
+ case Action::NOTIFY: {
+ if (action.event_name == "SessionEnd") {
+ sessions_.erase(action.session_id);
+ } else {
+ auto& session = sessions_[action.session_id];
+ auto cwd = action.cwd.value_or("/");
+ if (session.cwd != cwd) {
+ session.cwd = std::move(cwd);
+ session.name.clear();
+ }
+ auto desktop = action.desktop.value_or("0");
+ if (session.desktop != desktop) {
+ session.desktop = std::move(desktop);
+ session.name.clear();
+ }
+ if (action.event_name == "Notification" &&
+ action.notification_type.has_value()) {
+ if (action.notification_type.value() == "permission_prompt" ||
+ action.notification_type.value() == "elicitation_dialog") {
+ session.state = State::PROMPT;
+ } else if (action.notification_type.value() == "idle_prompt") {
+ session.state = State::IDLE;
+ }
+ } else if (action.event_name == "UserPromptSubmit") {
+ session.state = State::BUSY;
+ }
+ }
+ update_lines();
+ draw();
+ break;
+ }
+ case Action::RESET:
+ sessions_.clear();
+ update_lines();
+ draw();
+ break;
+ }
+ }
+
+ xcb_thread.join();
+ return true;
+ }
+
+ void notify(std::string const& session_id, std::string const& event_name,
+ std::optional<std::string> const& cwd,
+ std::optional<std::string> const& notification_type,
+ std::optional<std::string> const& desktop) override {
+ post(Action::NOTIFY, session_id, event_name, cwd, notification_type,
+ desktop);
+ }
+
+ private:
+ enum class Action : uint8_t {
+ QUIT,
+ DRAW,
+ RESIZE,
+ NOTIFY,
+ RESET,
+ };
+
+ struct Line {
+ std::vector<xcb_char2b_t> text;
+ xcb_query_text_extents_cookie_t text_extents_cookie;
+ uint16_t text_extents_width{0};
+ uint16_t text_extents_height{0};
+ uint16_t text_extents_offset{0};
+ uint32_t color{0};
+ };
+
+ struct ActionData {
+ Action action;
+ xcb_rectangle_t rect;
+ bool last;
+ std::string session_id;
+ std::string event_name;
+ std::optional<std::string> cwd;
+ std::optional<std::string> notification_type;
+ std::optional<std::string> desktop;
+
+ ActionData() = default;
+
+ explicit ActionData(Action action) : action(action) {
+ assert(action == Action::QUIT || action == Action::RESET);
+ }
+
+ ActionData(Action action, xcb_rectangle_t rect, bool last)
+ : action(action), rect(rect), last(last) {
+ assert(action == Action::DRAW);
+ }
+
+ ActionData(Action action, uint16_t width, uint16_t height)
+ : action(action),
+ rect({.x = 0, .y = 0, .width = width, .height = height}) {
+ assert(action == Action::RESIZE);
+ }
+
+ ActionData(Action action, std::string session_id, std::string event_name,
+ std::optional<std::string> cwd,
+ std::optional<std::string> notification_type,
+ std::optional<std::string> desktop)
+ : action(action),
+ session_id(std::move(session_id)),
+ event_name(std::move(event_name)),
+ cwd(std::move(cwd)),
+ notification_type(std::move(notification_type)),
+ desktop(std::move(desktop)) {
+ assert(action == Action::NOTIFY);
+ }
+ };
+
+ void run_xcb() {
+ while (true) {
+ xcb::generic_event event(xcb_wait_for_event(conn_.get()));
+ if (!event) {
+ auto err = xcb_connection_has_error(conn_.get());
+ if (err) {
+ std::cerr << "X connection had fatal error: " << err << '\n';
+ } else {
+ std::cerr << "X connection had fatal I/O error.\n";
+ }
+ break;
+ }
+ auto response_type = XCB_EVENT_RESPONSE_TYPE(event.get());
+ if (response_type == XCB_EXPOSE) {
+ auto* e = reinterpret_cast<xcb_expose_event_t*>(event.get());
+ if (e->window == wnd_->id()) {
+ xcb_rectangle_t rect;
+ rect.x = static_cast<int16_t>(e->x);
+ rect.y = static_cast<int16_t>(e->y);
+ rect.width = e->width;
+ rect.height = e->height;
+ post(Action::DRAW, rect, e->count == 0);
+ }
+ continue;
+ }
+ if (response_type == XCB_KEY_PRESS) {
+ auto* e = reinterpret_cast<xcb_key_press_event_t*>(event.get());
+ if (e->event == wnd_->id()) {
+ auto str = keyboard_->get_utf8(e);
+ if (str == "q" || str == "\x1b" /* Escape */) {
+ break;
+ }
+ if ((e->state & XCB_MOD_MASK_CONTROL) &&
+ str == "\x12" /* Ctrl + R */) {
+ post(Action::RESET);
+ }
+ }
+ continue;
+ }
+ if (response_type == XCB_CONFIGURE_NOTIFY) {
+ auto* e = reinterpret_cast<xcb_configure_notify_event_t*>(event.get());
+ if (e->window == wnd_->id()) {
+ post(Action::RESIZE, e->width, e->height);
+ }
+ continue;
+ }
+ if (response_type == XCB_REPARENT_NOTIFY) {
+ // Ignored, part of XCB_EVENT_MASK_STRUCTURE_NOTIFY
+ continue;
+ }
+ if (response_type == XCB_MAP_NOTIFY) {
+ // Ignored, part of XCB_EVENT_MASK_STRUCTURE_NOTIFY
+ continue;
+ }
+ if (keyboard_->handle_event(conn_.get(), event.get())) {
+ continue;
+ }
+ if (response_type == XCB_CLIENT_MESSAGE) {
+ auto* e = reinterpret_cast<xcb_client_message_event_t*>(event.get());
+ if (e->window == wnd_->id() && e->type == wm_protocols_->get() &&
+ e->format == 32) {
+ if (e->data.data32[0] == wm_delete_window_->get()) {
+ break;
+ }
+ }
+ continue;
+ }
+
+#ifndef NDEBUG
+ if (response_type == 0) {
+ auto* e = reinterpret_cast<xcb_generic_error_t*>(event.get());
+ std::cout << "Unhandled error: "
+ << xcb_event_get_error_label(e->error_code) << '\n';
+ } else {
+ std::cout << "Unhandled event: " << xcb_event_get_label(response_type)
+ << '\n';
+ }
+#endif
+ }
+
+ post(Action::QUIT);
+ }
+
+ template <class... Args>
+ void post(Args&&... args) {
+ bool notify;
+ {
+ std::scoped_lock lock(action_lock_);
+ notify = action_.empty();
+ action_.emplace_back(std::forward<Args>(args)...);
+ }
+ if (notify)
+ action_cond_.notify_one();
+ }
+
+ void update_names() {
+ if (sessions_.empty())
+ return;
+
+ if (std::ranges::all_of(sessions_,
+ [](auto& it) { return !it.second.name.empty(); })) {
+ return;
+ }
+
+ auto it = sessions_.begin();
+ std::string common_path{it->second.cwd};
+ for (++it; it != sessions_.end(); ++it) {
+ auto& cwd = it->second.cwd;
+ size_t len = std::min(common_path.size(), cwd.size());
+ size_t i = 0;
+ for (; i < len; ++i) {
+ if (common_path[i] != cwd[i])
+ break;
+ }
+ common_path.resize(i);
+ }
+
+ if (!common_path.empty() && common_path.back() != '/') {
+ auto last = common_path.rfind('/');
+ if (last != std::string::npos) {
+ common_path = common_path.substr(0, last + 1);
+ } else {
+ common_path = "/";
+ }
+ }
+
+ for (auto& pair : sessions_) {
+ pair.second.name.clear();
+ pair.second.name.append(pair.second.desktop);
+ pair.second.name.append(": ");
+ auto suffix = pair.second.cwd.substr(common_path.size());
+ auto end = suffix.find('/');
+ if (end != std::string::npos)
+ suffix = suffix.substr(0, end);
+ pair.second.name.append(suffix);
+ }
+ }
+
+ void update_lines() {
+ update_names();
+
+ std::vector<std::map<std::string, Session>::const_iterator> sort;
+ for (auto it = sessions_.begin(); it != sessions_.end(); ++it) {
+ sort.emplace_back(it);
+ }
+ std::ranges::sort(
+ sort, [](auto& a, auto& b) { return a->second.name < b->second.name; });
+
+ lines_.resize(sessions_.size());
+ for (size_t i = 0; i < sort.size(); ++i) {
+ std::string line;
+ line.append(sort[i]->second.name);
+ line.append(": ");
+ switch (sort[i]->second.state) {
+ case State::BUSY:
+ line.append("busy");
+ lines_[i].color = foreground_busy_->get();
+ break;
+ case State::IDLE:
+ line.append("idle");
+ lines_[i].color = foreground_idle_->get();
+ break;
+ case State::PROMPT:
+ line.append("prompt");
+ lines_[i].color = foreground_prompt_->get();
+ break;
+ }
+
+ lines_[i].text.resize(line.size());
+ for (size_t j = 0; j < line.size(); ++j) {
+ lines_[i].text[j].byte1 = 0;
+ lines_[i].text[j].byte2 = line[j];
+ }
+
+ lines_[i].text_extents_cookie =
+ xcb_query_text_extents(conn_.get(), font_->id(),
+ lines_[i].text.size(), lines_[i].text.data());
+ lines_[i].text_extents_width = 0;
+ }
+ }
+
+ void draw() {
+ xcb_rectangle_t r{0, 0, wnd_width_, wnd_height_};
+
+ uint32_t values[2];
+ values[0] = background_->get();
+ xcb_change_gc(conn_.get(), gc_->id(), XCB_GC_FOREGROUND, values);
+ xcb_poly_fill_rectangle(conn_.get(), wnd_->id(), gc_->id(), 1, &r);
+
+ uint16_t tot_height = 0;
+ uint16_t max_width = 0;
+ uint16_t margin = 2;
+
+ for (auto& line : lines_) {
+ if (line.text_extents_width == 0) {
+ xcb::reply<xcb_query_text_extents_reply_t> reply(
+ xcb_query_text_extents_reply(conn_.get(), line.text_extents_cookie,
+ nullptr));
+ if (reply) {
+ line.text_extents_width = reply->overall_width;
+ line.text_extents_height = reply->font_ascent + reply->font_descent;
+ line.text_extents_offset = reply->font_ascent;
+ } else {
+ line.text_extents_width = 1;
+ line.text_extents_height = 0;
+ line.text_extents_offset = 0;
+ }
+ }
+ if (tot_height != 0)
+ tot_height += margin;
+ tot_height += line.text_extents_height;
+ max_width = std::max(line.text_extents_width, max_width);
+ }
+
+ auto x = static_cast<int16_t>(
+ max_width < wnd_width_ ? (wnd_width_ - max_width) / 2 : 0);
+ auto y = static_cast<int16_t>(
+ tot_height < wnd_height_ ? (wnd_height_ - tot_height) / 2 : 0);
+
+ for (auto& line : lines_) {
+ values[0] = line.color;
+ values[1] = background_->get();
+ xcb_change_gc(conn_.get(), gc_->id(),
+ XCB_GC_FOREGROUND | XCB_GC_BACKGROUND, values);
+
+ xcb_image_text_16(conn_.get(), line.text.size(), wnd_->id(), gc_->id(), x,
+ static_cast<int16_t>(y + line.text_extents_offset),
+ line.text.data());
+
+ y = static_cast<int16_t>(y + line.text_extents_height + margin);
+ }
+
+ xcb_flush(conn_.get());
+ }
+
+ xcb::shared_conn conn_;
+ std::optional<xcb::Atoms::Reference> string_atom_;
+ std::optional<xcb::Atoms::Reference> wm_protocols_;
+ std::optional<xcb::Atoms::Reference> wm_delete_window_;
+ std::optional<xcb::Colors::Color> background_;
+ std::optional<xcb::Colors::Color> foreground_idle_;
+ std::optional<xcb::Colors::Color> foreground_busy_;
+ std::optional<xcb::Colors::Color> foreground_prompt_;
+ std::unique_ptr<xcb::Keyboard> keyboard_;
+ xcb::unique_wnd wnd_;
+ xcb::unique_gc gc_;
+ xcb::unique_font font_;
+ uint16_t wnd_width_{100};
+ uint16_t wnd_height_{150};
+
+ std::map<std::string, Session> sessions_;
+
+ std::vector<Line> lines_;
+
+ std::mutex action_lock_;
+ std::condition_variable action_cond_;
+ std::deque<ActionData> action_;
+
+ std::unique_ptr<Server> server_;
+};
+
+} // namespace
+
+int Monitor::run(std::optional<std::string> display,
+ std::optional<std::string> font_name) {
+ Ui ui;
+ if (!ui.init())
+ return EXIT_FAILURE;
+ return ui.run(std::move(display), std::move(font_name));
+}
diff --git a/src/monitor.hh b/src/monitor.hh
new file mode 100644
index 0000000..8861473
--- /dev/null
+++ b/src/monitor.hh
@@ -0,0 +1,13 @@
+#ifndef MONITOR_HH
+#define MONITOR_HH
+
+#include <optional>
+#include <string>
+
+class Monitor {
+ public:
+ static int run(std::optional<std::string> display,
+ std::optional<std::string> font_name);
+};
+
+#endif // MONITOR_HH
diff --git a/src/notify.cc b/src/notify.cc
new file mode 100644
index 0000000..2403dc6
--- /dev/null
+++ b/src/notify.cc
@@ -0,0 +1,59 @@
+#include "notify.hh"
+
+#include "dbus_common.hh"
+#include "find_desktop.hh"
+
+#include <iostream>
+#include <optional>
+#include <rapidjson/document.h>
+#include <rapidjson/error/en.h>
+#include <rapidjson/istreamwrapper.h>
+#include <sdbus-c++/sdbus-c++.h>
+#include <string>
+#include <utility>
+
+int Notify::run(std::optional<std::string> display) {
+ rapidjson::Document d;
+ rapidjson::IStreamWrapper stream(std::cin);
+ d.ParseStream(stream);
+ if (d.HasParseError()) {
+ std::cerr << "Error parsing input: " << d.GetErrorOffset() << ": "
+ << rapidjson::GetParseError_En(d.GetParseError()) << '\n';
+ return 2;
+ }
+
+ if (d.IsObject() && d.HasMember("session_id") && d["session_id"].IsString() &&
+ d.HasMember("hook_event_name") && d["hook_event_name"].IsString()) {
+ std::string session_id = d["session_id"].GetString();
+ std::string event_name = d["hook_event_name"].GetString();
+ std::optional<std::string> cwd;
+ if (d.HasMember("cwd") && d["cwd"].IsString()) {
+ cwd = d["cwd"].GetString();
+ }
+ std::optional<std::string> notification_type;
+ if (d.HasMember("notification_type") && d["notification_type"].IsString()) {
+ notification_type = d["notification_type"].GetString();
+ }
+
+ std::optional<std::string> desktop =
+ find_desktop(std::move(display), /* use_prefix */ false);
+
+ auto proxy = sdbus::createProxy(dbus::kServiceName, dbus::kObjectPath);
+
+ try {
+ proxy->callMethod("notify")
+ .onInterface(dbus::kInterfaceName)
+ .withArguments(session_id, event_name, cwd.value_or(""),
+ notification_type.value_or(""), desktop.value_or(""))
+ .dontExpectReply();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to notify: " << err.what() << '\n';
+ return 2;
+ }
+
+ return 0;
+ }
+
+ std::cerr << "Unexpected input." << '\n';
+ return 2;
+}
diff --git a/src/notify.hh b/src/notify.hh
new file mode 100644
index 0000000..11c16d7
--- /dev/null
+++ b/src/notify.hh
@@ -0,0 +1,12 @@
+#ifndef NOTIFY_HH
+#define NOTIFY_HH
+
+#include <optional>
+#include <string>
+
+class Notify {
+ public:
+ static int run(std::optional<std::string> display);
+};
+
+#endif // NOTIFY_HH
diff --git a/src/xcb_atoms.cc b/src/xcb_atoms.cc
new file mode 100644
index 0000000..29c131f
--- /dev/null
+++ b/src/xcb_atoms.cc
@@ -0,0 +1,81 @@
+#include "xcb_atoms.hh"
+
+#include "xcb_connection.hh"
+#include "xcb_event.hh"
+
+#include <cassert>
+#include <cstddef>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+namespace {
+
+class AtomsImpl : public Atoms {
+ public:
+ explicit AtomsImpl(shared_conn conn)
+ : conn_(std::move(conn)), storage_(std::make_shared<StorageImpl>()) {}
+
+ Reference get(std::string atom) override {
+ auto it = index_.find(atom);
+ size_t index;
+ if (it == index_.end()) {
+ index = cookie_.size();
+ cookie_.push_back(
+ xcb_intern_atom(conn_.get(), 0, atom.size(), atom.c_str()));
+ index_.emplace(std::move(atom), index);
+ } else {
+ index = it->second;
+ }
+ return {storage_, index};
+ }
+
+ bool sync() override {
+ std::vector<xcb_atom_t> atoms;
+ atoms.reserve(cookie_.size());
+ for (auto const& cookie : cookie_) {
+ xcb::reply<xcb_intern_atom_reply_t> reply(
+ xcb_intern_atom_reply(conn_.get(), cookie, nullptr));
+ if (!reply)
+ return false;
+ atoms.push_back(reply->atom);
+ }
+ storage_->set(std::move(atoms));
+ return true;
+ }
+
+ private:
+ class StorageImpl : public Storage {
+ public:
+ [[nodiscard]]
+ xcb_atom_t get(size_t id) const override {
+ assert(id < resolved_.size());
+ return resolved_[id];
+ }
+
+ void set(std::vector<xcb_atom_t> resolved) {
+ resolved_ = std::move(resolved);
+ }
+
+ private:
+ std::vector<xcb_atom_t> resolved_;
+ };
+
+ shared_conn conn_;
+ std::map<std::string, size_t> index_;
+ std::vector<xcb_intern_atom_cookie_t> cookie_;
+ std::shared_ptr<StorageImpl> storage_;
+};
+
+} // namespace
+
+std::unique_ptr<Atoms> Atoms::create(shared_conn conn) {
+ return std::make_unique<AtomsImpl>(std::move(conn));
+}
+
+} // namespace xcb
diff --git a/src/xcb_atoms.hh b/src/xcb_atoms.hh
new file mode 100644
index 0000000..f7a375e
--- /dev/null
+++ b/src/xcb_atoms.hh
@@ -0,0 +1,57 @@
+#ifndef XCB_ATOMS_HH
+#define XCB_ATOMS_HH
+
+#include "xcb_connection.hh"
+
+#include <memory>
+#include <string>
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+class Atoms {
+ protected:
+ class Storage;
+
+ public:
+ virtual ~Atoms() = default;
+
+ class Reference {
+ public:
+ xcb_atom_t get() { return atoms_->get(id_); }
+
+ Reference(std::shared_ptr<Storage> atoms, size_t id)
+ : atoms_(atoms), id_(id) {}
+
+ private:
+ std::shared_ptr<Storage> atoms_;
+ size_t id_;
+ };
+
+ [[nodiscard]]
+ virtual Reference get(std::string atom) = 0;
+
+ virtual bool sync() = 0;
+
+ [[nodiscard]]
+ static std::unique_ptr<Atoms> create(shared_conn conn);
+
+ protected:
+ Atoms() = default;
+ Atoms(Atoms const&) = delete;
+ Atoms& operator=(Atoms const&) = delete;
+
+ class Storage {
+ public:
+ virtual ~Storage() = default;
+
+ virtual xcb_atom_t get(size_t id) const = 0;
+
+ protected:
+ Storage() = default;
+ };
+};
+
+} // namespace xcb
+
+#endif // XCB_ATOMS_HH
diff --git a/src/xcb_colors.cc b/src/xcb_colors.cc
new file mode 100644
index 0000000..b7ffff3
--- /dev/null
+++ b/src/xcb_colors.cc
@@ -0,0 +1,96 @@
+#include "xcb_colors.hh"
+
+#include "xcb_connection.hh"
+#include "xcb_event.hh"
+
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <utility>
+#include <vector>
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+namespace {
+
+class ColorsImpl : public Colors {
+ public:
+ ColorsImpl(shared_conn conn, xcb_colormap_t colormap)
+ : conn_(std::move(conn)),
+ colormap_(colormap),
+ storage_(std::make_shared<StorageImpl>()) {}
+
+ Color get_with_fallback(uint8_t r, uint8_t g, uint8_t b,
+ uint32_t fallback) override {
+ auto key = make_key(r, g, b);
+ auto it = index_.find(key);
+ size_t index;
+ if (it == index_.end()) {
+ index = cookie_.size();
+ cookie_.push_back(xcb_alloc_color(
+ conn_.get(), colormap_, static_cast<uint16_t>(r) << 8,
+ static_cast<uint16_t>(g) << 8, static_cast<uint16_t>(b) << 8));
+ fallback_.push_back(fallback);
+ index_.emplace(key, index);
+ } else {
+ index = it->second;
+ }
+ return {storage_, index};
+ }
+
+ bool sync() override {
+ std::vector<uint32_t> colors;
+ colors.reserve(cookie_.size());
+ for (size_t i = 0; i < cookie_.size(); ++i) {
+ xcb::reply<xcb_alloc_color_reply_t> reply(
+ xcb_alloc_color_reply(conn_.get(), cookie_[i], nullptr));
+ if (reply) {
+ colors.push_back(reply->pixel);
+ } else {
+ colors.push_back(fallback_[i]);
+ }
+ }
+ storage_->set(std::move(colors));
+ return true;
+ }
+
+ private:
+ class StorageImpl : public Storage {
+ public:
+ [[nodiscard]]
+ uint32_t get(size_t id) const override {
+ assert(id < resolved_.size());
+ return resolved_[id];
+ }
+
+ void set(std::vector<uint32_t> resolved) {
+ resolved_ = std::move(resolved);
+ }
+
+ private:
+ std::vector<uint32_t> resolved_;
+ };
+
+ static uint32_t make_key(uint8_t r, uint8_t g, uint8_t b) {
+ return static_cast<uint32_t>(r) << 16 | static_cast<uint32_t>(g) << 8 | b;
+ }
+
+ shared_conn conn_;
+ xcb_colormap_t colormap_;
+ std::map<uint32_t, size_t> index_;
+ std::vector<xcb_alloc_color_cookie_t> cookie_;
+ std::vector<uint32_t> fallback_;
+ std::shared_ptr<StorageImpl> storage_;
+};
+
+} // namespace
+
+std::unique_ptr<Colors> Colors::create(shared_conn conn,
+ xcb_colormap_t colormap) {
+ return std::make_unique<ColorsImpl>(std::move(conn), colormap);
+}
+
+} // namespace xcb
diff --git a/src/xcb_colors.hh b/src/xcb_colors.hh
new file mode 100644
index 0000000..a1bc610
--- /dev/null
+++ b/src/xcb_colors.hh
@@ -0,0 +1,57 @@
+#ifndef XCB_COLORS_HH
+#define XCB_COLORS_HH
+
+#include "xcb_connection.hh"
+
+#include <memory>
+
+namespace xcb {
+
+class Colors {
+ protected:
+ class Storage;
+
+ public:
+ virtual ~Colors() = default;
+
+ class Color {
+ public:
+ uint32_t get() { return colors_->get(id_); }
+
+ Color(std::shared_ptr<Storage> colors, size_t id)
+ : colors_(colors), id_(id) {}
+
+ private:
+ std::shared_ptr<Storage> colors_;
+ size_t id_;
+ };
+
+ [[nodiscard]]
+ virtual Color get_with_fallback(uint8_t r, uint8_t g, uint8_t b,
+ uint32_t fallback) = 0;
+
+ virtual bool sync() = 0;
+
+ [[nodiscard]]
+ static std::unique_ptr<Colors> create(shared_conn conn,
+ xcb_colormap_t colormap);
+
+ protected:
+ Colors() = default;
+ Colors(Colors const&) = delete;
+ Colors& operator=(Colors const&) = delete;
+
+ class Storage {
+ public:
+ virtual ~Storage() = default;
+
+ virtual uint32_t get(size_t id) const = 0;
+
+ protected:
+ Storage() = default;
+ };
+};
+
+} // namespace xcb
+
+#endif // XCB_COLORS_HH
diff --git a/src/xcb_connection.cc b/src/xcb_connection.cc
new file mode 100644
index 0000000..89b4a63
--- /dev/null
+++ b/src/xcb_connection.cc
@@ -0,0 +1,25 @@
+#include "xcb_connection.hh"
+
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+shared_conn make_shared_conn(xcb_connection_t* conn) {
+ return {conn, internal::xcb_connection_deleter()};
+}
+
+unique_conn make_unique_conn(xcb_connection_t* conn) {
+ return unique_conn(conn);
+}
+
+xcb_screen_t* get_screen(xcb_connection_t* conn, int screen_index) {
+ auto iter = xcb_setup_roots_iterator(xcb_get_setup(conn));
+ for (; iter.rem; --screen_index, xcb_screen_next(&iter)) {
+ if (screen_index == 0) {
+ return iter.data;
+ }
+ }
+ return nullptr;
+}
+
+} // namespace xcb
diff --git a/src/xcb_connection.hh b/src/xcb_connection.hh
new file mode 100644
index 0000000..23801b8
--- /dev/null
+++ b/src/xcb_connection.hh
@@ -0,0 +1,28 @@
+#ifndef XCB_CONNECTION_HH
+#define XCB_CONNECTION_HH
+
+#include <memory>
+#include <xcb/xcb.h> // IWYU pragma: export
+
+namespace xcb {
+
+namespace internal {
+
+struct xcb_connection_deleter {
+ void operator()(xcb_connection_t* ptr) { xcb_disconnect(ptr); }
+};
+
+} // namespace internal
+
+typedef std::shared_ptr<xcb_connection_t> shared_conn;
+typedef std::unique_ptr<xcb_connection_t, internal::xcb_connection_deleter>
+ unique_conn;
+
+shared_conn make_shared_conn(xcb_connection_t* conn);
+unique_conn make_unique_conn(xcb_connection_t* conn);
+
+xcb_screen_t* get_screen(xcb_connection_t* conn, int screen_index);
+
+} // namespace xcb
+
+#endif // XCB_CONNECTION_HH
diff --git a/src/xcb_event.hh b/src/xcb_event.hh
new file mode 100644
index 0000000..0e0e10b
--- /dev/null
+++ b/src/xcb_event.hh
@@ -0,0 +1,30 @@
+#ifndef XCB_EVENT_HH
+#define XCB_EVENT_HH
+
+#include <memory>
+#include <stdlib.h>
+#include <xcb/xcb.h>
+#include <xcb/xcb_event.h>
+
+namespace xcb {
+
+namespace internal {
+
+struct FreeDeleter {
+ void operator()(void* ptr) { free(ptr); }
+};
+
+} // namespace internal
+
+typedef std::unique_ptr<xcb_generic_event_t, internal::FreeDeleter>
+ generic_event;
+
+typedef std::unique_ptr<xcb_generic_error_t, internal::FreeDeleter>
+ generic_error;
+
+template <typename T>
+using reply = std::unique_ptr<T, internal::FreeDeleter>;
+
+} // namespace xcb
+
+#endif // XCB_EVENT_HH
diff --git a/src/xcb_resource.cc b/src/xcb_resource.cc
new file mode 100644
index 0000000..8005c6f
--- /dev/null
+++ b/src/xcb_resource.cc
@@ -0,0 +1,41 @@
+#include "xcb_resource.hh"
+
+#include "xcb_connection.hh"
+
+#include <memory>
+#include <utility>
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+unique_wnd make_unique_wnd(shared_conn conn) {
+ return std::make_unique<xcb_resource<xcb_window_t, internal::WndDeleter>>(
+ std::move(conn));
+}
+
+shared_wnd make_shared_wnd(shared_conn conn) {
+ return std::make_shared<xcb_resource<xcb_window_t, internal::WndDeleter>>(
+ std::move(conn));
+}
+
+unique_gc make_unique_gc(shared_conn conn) {
+ return std::make_unique<xcb_resource<xcb_gcontext_t, internal::GCDeleter>>(
+ std::move(conn));
+}
+
+shared_gc make_shared_gc(shared_conn conn) {
+ return std::make_shared<xcb_resource<xcb_gcontext_t, internal::GCDeleter>>(
+ std::move(conn));
+}
+
+unique_font make_unique_font(shared_conn conn) {
+ return std::make_unique<xcb_resource<xcb_font_t, internal::FontDeleter>>(
+ std::move(conn));
+}
+
+shared_font make_shared_font(shared_conn conn) {
+ return std::make_shared<xcb_resource<xcb_font_t, internal::FontDeleter>>(
+ std::move(conn));
+}
+
+} // namespace xcb
diff --git a/src/xcb_resource.hh b/src/xcb_resource.hh
new file mode 100644
index 0000000..b499f2c
--- /dev/null
+++ b/src/xcb_resource.hh
@@ -0,0 +1,99 @@
+#ifndef XCB_RESOURCE_HH
+#define XCB_RESOURCE_HH
+
+#include "xcb_connection.hh"
+
+#include <memory>
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+namespace internal {
+
+struct WndDeleter {
+ void operator()(xcb_connection_t* conn, xcb_window_t wnd) const {
+ xcb_destroy_window(conn, wnd);
+ }
+};
+
+struct GCDeleter {
+ void operator()(xcb_connection_t* conn, xcb_gcontext_t gc) const {
+ xcb_free_gc(conn, gc);
+ }
+};
+
+struct FontDeleter {
+ void operator()(xcb_connection_t* conn, xcb_font_t font) const {
+ xcb_close_font(conn, font);
+ }
+};
+
+} // namespace internal
+
+template <typename T, typename Deleter>
+class xcb_resource {
+ public:
+ explicit xcb_resource(shared_conn conn)
+ : conn_(conn), id_(xcb_generate_id(conn_.get())) {}
+ constexpr xcb_resource() : id_(XCB_NONE) {}
+ xcb_resource(xcb_resource const& res) = delete;
+ xcb_resource(xcb_resource&& res) : conn_(res.conn_), id_(res.release()) {}
+ ~xcb_resource() { reset(); }
+
+ xcb_resource& operator=(xcb_resource const& res) = delete;
+ xcb_resource& operator=(xcb_resource&& res) {
+ reset();
+ conn_ = res.conn_;
+ id_ = res.release();
+ return *this;
+ }
+
+ T id() const { return id_; }
+
+ void reset() {
+ if (id_ == XCB_NONE)
+ return;
+ deleter_(conn_.get(), id_);
+ id_ = XCB_NONE;
+ }
+
+ T release() {
+ auto ret = id_;
+ id_ = XCB_NONE;
+ conn_.reset();
+ return ret;
+ }
+
+ private:
+ shared_conn conn_;
+ T id_;
+ Deleter const deleter_{};
+};
+
+typedef std::unique_ptr<xcb_resource<xcb_window_t, internal::WndDeleter>>
+ unique_wnd;
+typedef std::shared_ptr<xcb_resource<xcb_window_t, internal::WndDeleter>>
+ shared_wnd;
+
+unique_wnd make_unique_wnd(shared_conn conn);
+shared_wnd make_shared_wnd(shared_conn conn);
+
+typedef std::unique_ptr<xcb_resource<xcb_gcontext_t, internal::GCDeleter>>
+ unique_gc;
+typedef std::shared_ptr<xcb_resource<xcb_gcontext_t, internal::GCDeleter>>
+ shared_gc;
+
+unique_gc make_unique_gc(shared_conn conn);
+shared_gc make_shared_gc(shared_conn conn);
+
+typedef std::unique_ptr<xcb_resource<xcb_font_t, internal::FontDeleter>>
+ unique_font;
+typedef std::shared_ptr<xcb_resource<xcb_font_t, internal::FontDeleter>>
+ shared_font;
+
+unique_font make_unique_font(shared_conn conn);
+shared_font make_shared_font(shared_conn conn);
+
+} // namespace xcb
+
+#endif // XCB_RESOURCE_HH
diff --git a/src/xcb_resources.hh b/src/xcb_resources.hh
new file mode 100644
index 0000000..f0482c3
--- /dev/null
+++ b/src/xcb_resources.hh
@@ -0,0 +1,35 @@
+#ifndef XCB_RESOURCES_HH
+#define XCB_RESOURCES_HH
+
+#include "xcb_connection.hh"
+
+#include <memory>
+#include <optional>
+#include <string>
+
+namespace xcb {
+
+class Resources {
+ public:
+ virtual ~Resources() = default;
+
+ virtual std::optional<std::string> get_string(std::string const& name,
+ std::string const& clazz) = 0;
+
+ virtual std::optional<long> get_long(std::string const& name,
+ std::string const& clazz) = 0;
+
+ virtual std::optional<bool> get_bool(std::string const& name,
+ std::string const& clazz) = 0;
+
+ static std::unique_ptr<Resources> create(shared_conn conn);
+
+ protected:
+ Resources() = default;
+ Resources(Resources const&) = delete;
+ Resources& operator=(Resources const&) = delete;
+};
+
+} // namespace xcb
+
+#endif // XCB_RESOURCES_HH
diff --git a/src/xcb_resources_none.cc b/src/xcb_resources_none.cc
new file mode 100644
index 0000000..d9fa783
--- /dev/null
+++ b/src/xcb_resources_none.cc
@@ -0,0 +1,37 @@
+#include "xcb_resources.hh"
+
+#include <memory>
+#include <optional>
+#include <string>
+
+namespace xcb {
+
+namespace {
+
+class ResourcesNone : public Resources {
+ public:
+ ResourcesNone() = default;
+
+ std::optional<std::string> get_string(std::string const&,
+ std::string const&) override {
+ return std::nullopt;
+ }
+
+ std::optional<long> get_long(std::string const&,
+ std::string const&) override {
+ return std::nullopt;
+ }
+
+ std::optional<bool> get_bool(std::string const&,
+ std::string const&) override {
+ return std::nullopt;
+ }
+};
+
+} // namespace
+
+std::unique_ptr<Resources> Resources::create(shared_conn) {
+ return std::make_unique<ResourcesNone>();
+}
+
+} // namespace xcb
diff --git a/src/xcb_resources_xrm.cc b/src/xcb_resources_xrm.cc
new file mode 100644
index 0000000..1574fdc
--- /dev/null
+++ b/src/xcb_resources_xrm.cc
@@ -0,0 +1,74 @@
+#include "xcb_connection.hh"
+#include "xcb_resources.hh"
+
+#include <cstdlib>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <xcb/xcb_xrm.h>
+
+namespace xcb {
+
+namespace {
+
+struct DatabaseDeleter {
+ void operator()(xcb_xrm_database_t* db) { xcb_xrm_database_free(db); }
+};
+
+class ResourcesXrm : public Resources {
+ public:
+ explicit ResourcesXrm(shared_conn conn)
+ : conn_(std::move(conn)),
+ db_(xcb_xrm_database_from_default(conn_.get())) {}
+
+ std::optional<std::string> get_string(std::string const& name,
+ std::string const& clazz) override {
+ if (db_) {
+ char* tmp;
+ if (xcb_xrm_resource_get_string(db_.get(), std::string(name).c_str(),
+ std::string(clazz).c_str(), &tmp) >= 0) {
+ std::string ret(tmp);
+ free(tmp);
+ return ret;
+ }
+ }
+ return std::nullopt;
+ }
+
+ std::optional<long> get_long(std::string const& name,
+ std::string const& clazz) override {
+ if (db_) {
+ long tmp;
+ if (xcb_xrm_resource_get_long(db_.get(), std::string(name).c_str(),
+ std::string(clazz).c_str(), &tmp) >= 0) {
+ return tmp;
+ }
+ }
+ return std::nullopt;
+ }
+
+ std::optional<bool> get_bool(std::string const& name,
+ std::string const& clazz) override {
+ if (db_) {
+ bool tmp;
+ if (xcb_xrm_resource_get_bool(db_.get(), std::string(name).c_str(),
+ std::string(clazz).c_str(), &tmp) >= 0) {
+ return tmp;
+ }
+ }
+ return std::nullopt;
+ }
+
+ private:
+ shared_conn conn_;
+ std::unique_ptr<xcb_xrm_database_t, DatabaseDeleter> db_;
+};
+
+} // namespace
+
+std::unique_ptr<Resources> Resources::create(shared_conn conn) {
+ return std::make_unique<ResourcesXrm>(std::move(conn));
+}
+
+} // namespace xcb
diff --git a/src/xcb_xkb.cc b/src/xcb_xkb.cc
new file mode 100644
index 0000000..3d00537
--- /dev/null
+++ b/src/xcb_xkb.cc
@@ -0,0 +1,167 @@
+#include "xcb_xkb.hh"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <xcb/xcb.h>
+#include <xcb/xcb_event.h>
+#define explicit dont_use_cxx_explicit
+#include <xcb/xkb.h>
+#undef explicit
+#include <utility>
+#include <xcb/xproto.h>
+#include <xkbcommon/xkbcommon-x11.h>
+#include <xkbcommon/xkbcommon.h>
+
+namespace xcb {
+
+namespace {
+
+struct KeymapDeleter {
+ void operator()(xkb_keymap* keymap) const {
+ if (keymap)
+ xkb_keymap_unref(keymap);
+ }
+};
+
+struct StateDeleter {
+ void operator()(xkb_state* state) const {
+ if (state)
+ xkb_state_unref(state);
+ }
+};
+
+struct ContextDeleter {
+ void operator()(xkb_context* context) const {
+ if (context)
+ xkb_context_unref(context);
+ }
+};
+
+class KeyboardImpl : public Keyboard {
+ public:
+ KeyboardImpl() = default;
+
+ bool init(xcb_connection_t* conn) {
+ if (!xkb_x11_setup_xkb_extension(
+ conn, XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION,
+ XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, nullptr, nullptr,
+ &first_xkb_event_, nullptr))
+ return false;
+
+ ctx_.reset(xkb_context_new(XKB_CONTEXT_NO_FLAGS));
+ if (!ctx_)
+ return false;
+ device_id_ = xkb_x11_get_core_keyboard_device_id(conn);
+ if (device_id_ == -1)
+ return false;
+ if (!update_keymap(conn))
+ return false;
+ select_events(conn);
+ return true;
+ }
+
+ bool handle_event(xcb_connection_t* conn,
+ xcb_generic_event_t* event) override {
+ if (XCB_EVENT_RESPONSE_TYPE(event) == first_xkb_event_) {
+ auto* xkb_event = reinterpret_cast<xkb_generic_event_t*>(event);
+ if (std::cmp_equal(xkb_event->deviceID, device_id_)) {
+ switch (xkb_event->xkbType) {
+ case XCB_XKB_NEW_KEYBOARD_NOTIFY: {
+ auto* e =
+ reinterpret_cast<xcb_xkb_new_keyboard_notify_event_t*>(event);
+ if (e->changed & XCB_XKB_NKN_DETAIL_KEYCODES)
+ update_keymap(conn);
+ break;
+ }
+ case XCB_XKB_MAP_NOTIFY:
+ update_keymap(conn);
+ break;
+ case XCB_XKB_STATE_NOTIFY: {
+ auto* e = reinterpret_cast<xcb_xkb_state_notify_event_t*>(event);
+ xkb_state_update_mask(state_.get(), e->baseMods, e->latchedMods,
+ e->lockedMods, e->baseGroup, e->latchedGroup,
+ e->lockedGroup);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ std::string get_utf8(xcb_key_press_event_t* event) override {
+ char tmp[16];
+ xkb_state_key_get_utf8(state_.get(), event->detail, tmp, sizeof(tmp));
+ return {tmp};
+ }
+
+ private:
+ struct xkb_generic_event_t {
+ uint8_t response_type;
+ uint8_t xkbType;
+ uint16_t sequence;
+ xcb_timestamp_t time;
+ uint8_t deviceID;
+ };
+
+ bool update_keymap(xcb_connection_t* conn) {
+ auto* keymap = xkb_x11_keymap_new_from_device(ctx_.get(), conn, device_id_,
+ XKB_KEYMAP_COMPILE_NO_FLAGS);
+ if (!keymap)
+ return false;
+ auto* state = xkb_x11_state_new_from_device(keymap, conn, device_id_);
+ if (!state) {
+ xkb_keymap_unref(keymap);
+ return false;
+ }
+ keymap_.reset(keymap);
+ state_.reset(state);
+ return true;
+ }
+
+ void select_events(xcb_connection_t* conn) const {
+ static const uint16_t new_keyboard_details = XCB_XKB_NKN_DETAIL_KEYCODES;
+ static const uint16_t map_parts =
+ XCB_XKB_MAP_PART_KEY_TYPES | XCB_XKB_MAP_PART_KEY_SYMS |
+ XCB_XKB_MAP_PART_MODIFIER_MAP | XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS |
+ XCB_XKB_MAP_PART_KEY_ACTIONS | XCB_XKB_MAP_PART_VIRTUAL_MODS |
+ XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP;
+ static const uint16_t state_details =
+ XCB_XKB_STATE_PART_MODIFIER_BASE | XCB_XKB_STATE_PART_MODIFIER_LATCH |
+ XCB_XKB_STATE_PART_MODIFIER_LOCK | XCB_XKB_STATE_PART_GROUP_BASE |
+ XCB_XKB_STATE_PART_GROUP_LATCH | XCB_XKB_STATE_PART_GROUP_LOCK;
+
+ xcb_xkb_select_events_details_t details = {};
+ details.affectNewKeyboard = new_keyboard_details;
+ details.newKeyboardDetails = new_keyboard_details;
+ details.affectState = state_details;
+ details.stateDetails = state_details;
+
+ xcb_xkb_select_events_aux(conn, device_id_,
+ XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY |
+ XCB_XKB_EVENT_TYPE_MAP_NOTIFY |
+ XCB_XKB_EVENT_TYPE_STATE_NOTIFY,
+ 0, 0, map_parts, map_parts, &details);
+ }
+
+ std::unique_ptr<xkb_context, ContextDeleter> ctx_;
+ std::unique_ptr<xkb_keymap, KeymapDeleter> keymap_;
+ std::unique_ptr<xkb_state, StateDeleter> state_;
+ uint8_t first_xkb_event_;
+ int32_t device_id_;
+};
+
+} // namespace
+
+std::unique_ptr<Keyboard> Keyboard::create(xcb_connection_t* conn) {
+ auto ret = std::make_unique<KeyboardImpl>();
+ if (ret->init(conn))
+ return ret;
+ return nullptr;
+}
+
+} // namespace xcb
diff --git a/src/xcb_xkb.hh b/src/xcb_xkb.hh
new file mode 100644
index 0000000..b05ce3b
--- /dev/null
+++ b/src/xcb_xkb.hh
@@ -0,0 +1,30 @@
+#ifndef XCB_XKB_HH
+#define XCB_XKB_HH
+
+#include <memory>
+#include <string>
+#include <xcb/xcb.h>
+#include <xcb/xproto.h>
+
+namespace xcb {
+
+class Keyboard {
+ public:
+ virtual ~Keyboard() = default;
+
+ virtual bool handle_event(xcb_connection_t* conn,
+ xcb_generic_event_t* event) = 0;
+
+ virtual std::string get_utf8(xcb_key_press_event_t* event) = 0;
+
+ static std::unique_ptr<Keyboard> create(xcb_connection_t* conn);
+
+ protected:
+ Keyboard() = default;
+ Keyboard(Keyboard const&) = delete;
+ Keyboard& operator=(Keyboard const&) = delete;
+};
+
+} // namespace xcb
+
+#endif // XCB_XKB_HH