diff options
| -rw-r--r-- | .clang-format | 315 | ||||
| -rw-r--r-- | .clang-tidy | 2 | ||||
| -rw-r--r-- | .dir-locals.el | 13 | ||||
| -rw-r--r-- | meson.build | 94 | ||||
| -rw-r--r-- | src/args.cc | 389 | ||||
| -rw-r--r-- | src/args.hh | 64 | ||||
| -rw-r--r-- | src/dbus_common.cc | 7 | ||||
| -rw-r--r-- | src/dbus_common.hh | 14 | ||||
| -rw-r--r-- | src/find_desktop.cc | 152 | ||||
| -rw-r--r-- | src/find_desktop.hh | 10 | ||||
| -rw-r--r-- | src/main.cc | 59 | ||||
| -rw-r--r-- | src/monitor.cc | 626 | ||||
| -rw-r--r-- | src/monitor.hh | 13 | ||||
| -rw-r--r-- | src/notify.cc | 59 | ||||
| -rw-r--r-- | src/notify.hh | 12 | ||||
| -rw-r--r-- | src/xcb_atoms.cc | 81 | ||||
| -rw-r--r-- | src/xcb_atoms.hh | 57 | ||||
| -rw-r--r-- | src/xcb_colors.cc | 96 | ||||
| -rw-r--r-- | src/xcb_colors.hh | 57 | ||||
| -rw-r--r-- | src/xcb_connection.cc | 25 | ||||
| -rw-r--r-- | src/xcb_connection.hh | 28 | ||||
| -rw-r--r-- | src/xcb_event.hh | 30 | ||||
| -rw-r--r-- | src/xcb_resource.cc | 41 | ||||
| -rw-r--r-- | src/xcb_resource.hh | 99 | ||||
| -rw-r--r-- | src/xcb_resources.hh | 35 | ||||
| -rw-r--r-- | src/xcb_resources_none.cc | 37 | ||||
| -rw-r--r-- | src/xcb_resources_xrm.cc | 74 | ||||
| -rw-r--r-- | src/xcb_xkb.cc | 167 | ||||
| -rw-r--r-- | src/xcb_xkb.hh | 30 |
29 files changed, 2686 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..aa4a4dc --- /dev/null +++ b/.clang-format @@ -0,0 +1,315 @@ +--- +Language: Cpp +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignArrayOfStructures: None +AlignConsecutiveAssignments: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionDeclarations: false + AlignFunctionPointers: false + PadOperators: true +AlignConsecutiveBitFields: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionDeclarations: false + AlignFunctionPointers: false + PadOperators: false +AlignConsecutiveDeclarations: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionDeclarations: true + AlignFunctionPointers: false + PadOperators: false +AlignConsecutiveMacros: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionDeclarations: false + AlignFunctionPointers: false + PadOperators: false +AlignConsecutiveShortCaseStatements: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCaseArrows: false + AlignCaseColons: false +AlignConsecutiveTableGenBreakingDAGArgColons: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionDeclarations: false + AlignFunctionPointers: false + PadOperators: false +AlignConsecutiveTableGenCondOperatorColons: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionDeclarations: false + AlignFunctionPointers: false + PadOperators: false +AlignConsecutiveTableGenDefinitionColons: + Enabled: false + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + AlignFunctionDeclarations: false + AlignFunctionPointers: false + PadOperators: false +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: + Kind: Always + OverEmptyLines: 0 +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowBreakBeforeNoexceptSpecifier: Never +AllowShortBlocksOnASingleLine: Never +AllowShortCaseExpressionOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: false +AllowShortCompoundRequirementOnASingleLine: true +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AllowShortNamespacesOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: BinPack +BitFieldColonSpacing: Both +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterExternBlock: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BracedInitializerIndentWidth: 2 +BreakAdjacentStringLiterals: true +BreakAfterAttributes: Leave +BreakAfterJavaFieldAnnotations: false +BreakAfterReturnType: None +BreakArrays: true +BreakBeforeBinaryOperators: None +BreakBeforeConceptDeclarations: Always +BreakBeforeBraces: Attach +BreakBeforeInlineASMColon: OnlyMultiline +BreakBeforeTernaryOperators: true +BreakBinaryOperations: Never +BreakConstructorInitializers: BeforeColon +BreakFunctionDefinitionParameters: false +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +BreakTemplateDeclarations: Yes +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: true +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '([-_](lzma|z|test|unittest))?$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: true +IndentExportBlock: true +IndentExternBlock: AfterExternBlock +IndentGotoLabels: true +IndentPPDirectives: AfterHash +IndentRequiresClause: true +IndentWidth: 2 +IndentWrappedFunctionNames: false +InsertBraces: false +InsertNewlineAtEOF: false +InsertTrailingCommas: None +IntegerLiteralSeparator: + Binary: 0 + BinaryMinDigits: 0 + Decimal: 0 + DecimalMinDigits: 0 + Hex: 0 + HexMinDigits: 0 +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLines: + AtEndOfFile: false + AtStartOfBlock: false + AtStartOfFile: true +KeepFormFeed: false +LambdaBodyIndentation: Signature +LineEnding: DeriveLF +MacroBlockBegin: '' +MacroBlockEnd: '' +MainIncludeChar: Quote +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PackConstructorInitializers: NextLine +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakBeforeMemberAccess: 150 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakScopeResolution: 500 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyIndentedWhitespace: 0 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +PPIndentWidth: 1 +QualifierAlignment: Leave +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + - ParseTestProto + - ParsePartialTestProto + CanonicalDelimiter: pb + BasedOnStyle: google +ReferenceAlignment: Pointer +ReflowComments: IndentOnly +RemoveBracesLLVM: false +RemoveEmptyLinesInUnwrappedLines: false +RemoveParentheses: Leave +RemoveSemicolon: false +RequiresClausePosition: OwnLine +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SkipMacroDefinitionBody: false +SortIncludes: CaseSensitive +SortJavaStaticImport: Before +SortUsingDeclarations: LexicographicNumeric +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeJsonColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false + AfterFunctionDeclarationName: false + AfterIfMacros: true + AfterOverloadedOperator: false + AfterPlacementOperator: true + AfterRequiresInClause: false + AfterRequiresInExpression: false + BeforeNonEmptyParentheses: false +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: Never +SpacesInContainerLiterals: true +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParens: Never +SpacesInParensOptions: + ExceptDoubleParentheses: false + InCStyleCasts: false + InConditionalStatements: false + InEmptyParentheses: false + Other: false +SpacesInSquareBrackets: false +Standard: Auto +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TableGenBreakInsideDAGArg: DontBreak +TabWidth: 8 +UseTab: Never +VerilogBreakBetweenInstancePorts: true +WhitespaceSensitiveMacros: + - BOOST_PP_STRINGIZE + - CF_SWIFT_NAME + - NS_SWIFT_NAME + - PP_STRINGIZE + - STRINGIZE +WrapNamespaceBodyWithEmptyLines: Leave +... + diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..27aad9a --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,2 @@ +--- +Checks: 'bugprone-*,misc-*,modernize-*,performance-*,portability-*,readability-*,-bugprone-easily-swappable-parameters,-bugprone-unchecked-optional-access,-misc-non-private-member-variables-in-classes,-misc-const-correctness,-misc-no-recursion,-modernize-avoid-c-arrays,-modernize-use-trailing-return-type,-readability-magic-numbers,-readability-identifier-length,-readability-braces-around-statements,-readability-function-cognitive-complexity,-readability-redundant-inline-specifier,-readability-implicit-bool-conversion,-readability-qualified-auto,-misc-header-include-cycle' diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..b834c07 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,13 @@ +;;; Directory Local Variables -*- no-byte-compile: t; -*- +;;; For more information see (info "(emacs) Directory Variables") + +((c++-mode . ((eval + . + (let ((project-path + (locate-dominating-file default-directory ".dir-locals.el"))) + (setq-local flycheck-clangcheck-build-path + (concat project-path "build")) + (setq-local flycheck-clang-language-standard "c++23") + (setq-local flycheck-cppcheck-standards '("c++23")) + (setq-local flycheck-clang-definitions '("HAVE_CONFIG_H")) + (setq-local flycheck-clang-include-path '("../src" "../build"))))))) diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..0146e90 --- /dev/null +++ b/meson.build @@ -0,0 +1,94 @@ +project( + 'claudemon', + 'cpp', + version : '0.1', + meson_version : '>= 1.3.0', + default_options : [ + 'warning_level=3', + 'cpp_std=c++23', + 'cpp_rtti=false', + ], +) + +cpp_optional_flags = [ + # Redefining explicit to be able to include xkb.h + '-Wno-keyword-macro', +] + +cpp_flags = [ + '-DVERSION="' + meson.project_version() + '"' +] +if get_option('buildtype') == 'release' + # If asserts are disabled parameters and variables used for only that + # end up causing warnings + cpp_optional_flags += ['-Wno-unused-parameter', '-Wno-unused-variable', + '-Wno-unused-but-set-variable'] + + cpp_flags += '-DNDEBUG' +endif + +cpp = meson.get_compiler('cpp') +cpp_flags += cpp.get_supported_arguments(cpp_optional_flags) +add_project_arguments(cpp_flags, language: 'cpp') + +thread_dep = dependency('threads') + +xcb_dep = [dependency('xcb', version: '>= 1.14'), + dependency('xcb-xkb', version: '>= 1.14'), + dependency('xcb-event', version: '>= 0.4.0'), + dependency('xcb-icccm', version: '>= 0.4.1'), + dependency('xcb-keysyms', version: '>= 0.4.0'), + dependency('xkbcommon-x11', version: '>= 1.0.3')] + +xcb_xrm_dep = dependency('xcb-xrm', version: '>= 1.0', required: false) + +dbus_dep = dependency('sdbus-c++', version: '>= 2.0.0') + +json_dep = dependency('RapidJSON', version: '>= 1.1.0') + +dependencies = [ + dbus_dep, + json_dep, + thread_dep, + xcb_dep, + xcb_xrm_dep, +] + +sources = [ + 'src/args.cc', + 'src/args.hh', + 'src/dbus_common.cc', + 'src/dbus_common.hh', + 'src/find_desktop.cc', + 'src/find_desktop.hh', + 'src/main.cc', + 'src/monitor.cc', + 'src/monitor.hh', + 'src/notify.cc', + 'src/notify.hh', + 'src/xcb_atoms.cc', + 'src/xcb_atoms.hh', + 'src/xcb_colors.cc', + 'src/xcb_colors.hh', + 'src/xcb_connection.cc', + 'src/xcb_connection.hh', + 'src/xcb_resource.cc', + 'src/xcb_resource.hh', + 'src/xcb_xkb.cc', + 'src/xcb_xkb.hh', +] + +if xcb_xrm_dep.found() + sources += 'src/xcb_resources_xrm.cc' +else + sources += 'src/xcb_resources_none.cc' +endif + +exe = executable( + 'claudemon', + sources, + install : true, + dependencies : dependencies, +) + +test('basic', exe) 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 |
