Deamon management

This commit is contained in:
2026-10-17 03:53:51 +02:00
parent 335a4674d1
commit b77b4a3fc6
35 changed files with 2001 additions and 172 deletions

134
shared/keymap.cpp Normal file
View File

@@ -0,0 +1,134 @@
// gKeyMapper - keyboard layout lookups (xkbcommon).
#include "keymap.h"
#include <cctype>
#include <cstdio>
#include <fstream>
#include <xkbcommon/xkbcommon.h>
#include "keys.h"
namespace gkeymapper {
namespace {
// xkbcommon uses X11 keycodes, which are the evdev codes offset by 8.
constexpr int kXkbKeycodeOffset = 8;
// Reads XKBLAYOUT from /etc/default/keyboard, e.g. "de".
std::string read_layout_name() {
std::ifstream in("/etc/default/keyboard");
std::string line;
while (std::getline(in, line)) {
const std::string key = "XKBLAYOUT=";
auto pos = line.find(key);
if (pos == std::string::npos) {
continue;
}
std::string value = line.substr(pos + key.size());
if (!value.empty() && value.front() == '"') {
value.erase(0, 1);
}
if (!value.empty() && value.back() == '"') {
value.pop_back();
}
return value;
}
return std::string();
}
} // namespace
Keymap::~Keymap() {
if (state_) {
xkb_state_unref(state_);
}
if (keymap_) {
xkb_keymap_unref(keymap_);
}
}
bool Keymap::init() {
layoutName_ = read_layout_name();
if (layoutName_.empty()) {
std::fprintf(stderr, "gkeymapper: no XKBLAYOUT, using US key positions\n");
return false;
}
xkb_context* context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if (!context) {
return false;
}
xkb_rule_names names{};
names.rules = "evdev";
names.model = "pc105";
names.layout = layoutName_.c_str();
names.variant = "";
names.options = "";
keymap_ = xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
xkb_context_unref(context);
if (!keymap_) {
std::fprintf(stderr, "gkeymapper: unknown layout '%s', using US key positions\n",
layoutName_.c_str());
return false;
}
state_ = xkb_state_new(keymap_);
if (!state_) {
return false;
}
return true;
}
int Keymap::key_code(const std::string& name) const {
if (!state_) {
return linux_key_code(name);
}
xkb_keysym_t keysym =
xkb_keysym_from_name(name.c_str(), XKB_KEYSYM_CASE_INSENSITIVE);
if (keysym != XKB_KEY_NoSymbol) {
for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap_);
code <= xkb_keymap_max_keycode(keymap_); ++code) {
if (xkb_state_key_get_one_sym(state_, code) == keysym) {
int evdev = static_cast<int>(code) - kXkbKeycodeOffset;
if (evdev > 0) {
return evdev;
}
}
}
}
// Not present on this layout (e.g. "semicolon" on a German keyboard) or
// unknown: fall back to the US position.
return linux_key_code(name);
}
std::string Keymap::base_key_name(unsigned int keycode) const {
if (!state_) {
return std::string();
}
xkb_keysym_t keysym = xkb_state_key_get_one_sym(state_, keycode);
if (keysym == XKB_KEY_NoSymbol) {
return std::string();
}
char buffer[64];
int written = xkb_keysym_get_name(keysym, buffer, sizeof(buffer));
if (written <= 0) {
return std::string();
}
std::string name(buffer);
for (char& c : name) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return name;
}
} // namespace gkeymapper

51
shared/keymap.h Normal file
View File

@@ -0,0 +1,51 @@
// gKeyMapper - keyboard layout lookups (xkbcommon), shared by GUI and daemon.
//
// The active layout is read from /etc/default/keyboard (XKBLAYOUT). Without it,
// names fall back to the layout-independent US key positions in shared/keys.
#ifndef GKEYMAPPER_KEYMAP_H
#define GKEYMAPPER_KEYMAP_H
#include <string>
struct xkb_keymap;
struct xkb_state;
namespace gkeymapper {
// Two-way lookup on the active keyboard layout:
// - daemon: canonical key name -> Linux input (evdev) code
// - GUI: pressed X keycode -> canonical base key name (level 0)
class Keymap {
public:
Keymap() = default;
~Keymap();
Keymap(const Keymap&) = delete;
Keymap& operator=(const Keymap&) = delete;
// Builds the keymap from /etc/default/keyboard. Returns false if it cannot
// be built; the lookups then use the US fallback.
bool init();
bool is_ready() const { return state_ != nullptr; }
const std::string& layout_name() const { return layoutName_; }
// Canonical key name -> Linux input (evdev) code, e.g. "z" -> 21 on a German
// layout. Falls back to linux_key_code() for names that are not part of the
// layout.
int key_code(const std::string& name) const;
// X keycode (same value GDK reports) -> level-0 keysym name, lowercased
// ("ssharp", "a", "f4"). Empty if it cannot be resolved. Used by the capture
// dialog so "shift+ssharf" is recorded instead of the shifted symbol.
std::string base_key_name(unsigned int keycode) const;
private:
xkb_keymap* keymap_ = nullptr;
xkb_state* state_ = nullptr;
std::string layoutName_;
};
} // namespace gkeymapper
#endif // GKEYMAPPER_KEYMAP_H

110
shared/keys.cpp Normal file
View File

@@ -0,0 +1,110 @@
// gKeyMapper - canonical key name tables.
#include "keys.h"
#include <linux/input-event-codes.h>
namespace gkeymapper {
namespace {
const KeyDef kKeys[] = {
// Modifiers (also needed as key codes when a shortcut is replayed).
{"ctrl", KEY_LEFTCTRL},
{"control", KEY_LEFTCTRL},
{"alt", KEY_LEFTALT},
{"shift", KEY_LEFTSHIFT},
{"super", KEY_LEFTMETA},
{"meta", KEY_LEFTMETA},
{"altgr", KEY_RIGHTALT},
{"iso_level3_shift", KEY_RIGHTALT},
// Letters.
{"a", KEY_A}, {"b", KEY_B}, {"c", KEY_C}, {"d", KEY_D},
{"e", KEY_E}, {"f", KEY_F}, {"g", KEY_G}, {"h", KEY_H},
{"i", KEY_I}, {"j", KEY_J}, {"k", KEY_K}, {"l", KEY_L},
{"m", KEY_M}, {"n", KEY_N}, {"o", KEY_O}, {"p", KEY_P},
{"q", KEY_Q}, {"r", KEY_R}, {"s", KEY_S}, {"t", KEY_T},
{"u", KEY_U}, {"v", KEY_V}, {"w", KEY_W}, {"x", KEY_X},
{"y", KEY_Y}, {"z", KEY_Z},
// Digits.
{"0", KEY_0}, {"1", KEY_1}, {"2", KEY_2}, {"3", KEY_3}, {"4", KEY_4},
{"5", KEY_5}, {"6", KEY_6}, {"7", KEY_7}, {"8", KEY_8}, {"9", KEY_9},
// Function keys.
{"f1", KEY_F1}, {"f2", KEY_F2}, {"f3", KEY_F3}, {"f4", KEY_F4},
{"f5", KEY_F5}, {"f6", KEY_F6}, {"f7", KEY_F7}, {"f8", KEY_F8},
{"f9", KEY_F9}, {"f10", KEY_F10}, {"f11", KEY_F11}, {"f12", KEY_F12},
// Editing / navigation.
{"escape", KEY_ESC},
{"tab", KEY_TAB},
{"space", KEY_SPACE},
{"return", KEY_ENTER},
{"backspace", KEY_BACKSPACE},
{"delete", KEY_DELETE},
{"insert", KEY_INSERT},
{"home", KEY_HOME},
{"end", KEY_END},
{"page_up", KEY_PAGEUP},
{"page_down", KEY_PAGEDOWN},
{"up", KEY_UP},
{"down", KEY_DOWN},
{"left", KEY_LEFT},
{"right", KEY_RIGHT},
{"caps_lock", KEY_CAPSLOCK},
// Punctuation.
{"minus", KEY_MINUS},
{"equal", KEY_EQUAL},
{"bracketleft", KEY_LEFTBRACE},
{"bracketright", KEY_RIGHTBRACE},
{"semicolon", KEY_SEMICOLON},
{"apostrophe", KEY_APOSTROPHE},
{"grave", KEY_GRAVE},
{"backslash", KEY_BACKSLASH},
{"comma", KEY_COMMA},
{"period", KEY_DOT},
{"slash", KEY_SLASH},
};
} // namespace
const KeyDef* key_definitions() {
return kKeys;
}
int key_definition_count() {
return static_cast<int>(sizeof(kKeys) / sizeof(kKeys[0]));
}
int linux_key_code(const std::string& name) {
for (const KeyDef& def : kKeys) {
if (name == def.name) {
return def.linux_code;
}
}
return -1;
}
unsigned modifier_bit(const std::string& name) {
if (name == "ctrl" || name == "control") {
return kModCtrl;
}
if (name == "alt") {
return kModAlt;
}
if (name == "shift") {
return kModShift;
}
if (name == "super" || name == "meta") {
return kModSuper;
}
if (name == "altgr" || name == "iso_level3_shift") {
return kModAltGr;
}
return kModNone;
}
} // namespace gkeymapper

41
shared/keys.h Normal file
View File

@@ -0,0 +1,41 @@
// gKeyMapper - canonical key names shared by GUI and daemon.
//
// Canonical names are the lowercase GDK keysym names the GUI already writes
// (e.g. "f4", "escape", "page_up", "ctrl"). The daemon maps the same names to
// Linux input codes, so both halves of the project agree on the spelling.
#ifndef GKEYMAPPER_KEYS_H
#define GKEYMAPPER_KEYS_H
#include <string>
namespace gkeymapper {
// Modifier bits used by the shortcut grammar ("ctrl+c", "altgr+7").
enum ModifierBit : unsigned {
kModNone = 0,
kModCtrl = 1u << 0,
kModAlt = 1u << 1,
kModShift = 1u << 2,
kModSuper = 1u << 3,
kModAltGr = 1u << 4,
};
struct KeyDef {
const char* name; // canonical, lowercase; may also be a modifier name
int linux_code; // KEY_* from <linux/input-event-codes.h>
};
// All supported keys (regular keys and modifiers), no particular order.
const KeyDef* key_definitions();
int key_definition_count();
// Linux KEY_* code for a canonical name, or -1 if unknown.
int linux_key_code(const std::string& name);
// Modifier bit for "ctrl"/"alt"/"shift"/"super" ("meta" counts as super),
// 0 for everything else.
unsigned modifier_bit(const std::string& name);
} // namespace gkeymapper
#endif // GKEYMAPPER_KEYS_H

193
shared/mapping.cpp Normal file
View File

@@ -0,0 +1,193 @@
// gKeyMapper - mappings file parsing/formatting.
#include "mapping.h"
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include "keys.h"
namespace gkeymapper {
const char* action_type_token(ActionType type) {
switch (type) {
case ActionType::Key:
return "key";
case ActionType::Command:
return "command";
case ActionType::Disabled:
default:
return "disabled";
}
}
ActionType action_type_from_token(const std::string& token) {
if (token == "key") {
return ActionType::Key;
}
if (token == "command") {
return ActionType::Command;
}
return ActionType::Disabled;
}
std::string format_mappings(const MappingSet& set) {
std::string out = "# gKeyMapper mappings (G1..G9)\n";
for (int i = 0; i < kNumKeys; ++i) {
Mapping mapping;
if (i < static_cast<int>(set.keys.size())) {
mapping = set.keys[i];
}
out += "G";
out += std::to_string(i + 1);
out += "=";
out += action_type_token(mapping.type);
out += ":";
out += mapping.target;
out += "\n";
}
return out;
}
MappingSet parse_mappings(const std::string& text) {
MappingSet set;
set.keys.assign(kNumKeys, Mapping{});
std::istringstream in(text);
std::string line;
while (std::getline(in, line)) {
if (line.empty() || line[0] == '#') {
continue;
}
auto eq = line.find('=');
if (eq == std::string::npos) {
continue;
}
std::string key = line.substr(0, eq);
if (key.size() < 2 || key[0] != 'G') {
continue;
}
int n = 0;
try {
n = std::stoi(key.substr(1)) - 1;
} catch (...) {
continue;
}
if (n < 0 || n >= kNumKeys) {
continue;
}
std::string rest = line.substr(eq + 1);
auto col = rest.find(':');
std::string typeToken = (col == std::string::npos) ? rest : rest.substr(0, col);
std::string target = (col == std::string::npos) ? std::string() : rest.substr(col + 1);
set.keys[n].type = action_type_from_token(typeToken);
set.keys[n].target = target;
}
return set;
}
bool load_mappings_file(const std::string& path, MappingSet& out) {
std::ifstream in(path);
if (!in) {
return false;
}
std::stringstream buffer;
buffer << in.rdbuf();
out = parse_mappings(buffer.str());
return true;
}
bool save_mappings_file(const std::string& path, const MappingSet& set) {
// Write a temporary file first and rename it into place: a reader (the
// daemon) either sees the old or the new file, never a half-written one.
const std::string tempPath = path + ".tmp";
{
std::ofstream out(tempPath);
if (!out) {
return false;
}
out << format_mappings(set);
out.flush();
if (!out) {
return false;
}
}
return std::rename(tempPath.c_str(), path.c_str()) == 0;
}
std::string default_config_path() {
const char* home = std::getenv("HOME");
return std::string(home ? home : "") + "/.config/gKeyMapper/mappings.conf";
}
std::string format_shortcut_step(unsigned modifiers, const std::string& key_name) {
std::string step;
if ((modifiers & kModCtrl) != 0) {
step += "ctrl+";
}
if ((modifiers & kModAlt) != 0) {
step += "alt+";
}
if ((modifiers & kModShift) != 0) {
step += "shift+";
}
if ((modifiers & kModSuper) != 0) {
step += "super+";
}
if ((modifiers & kModAltGr) != 0) {
step += "altgr+";
}
step += key_name;
return step;
}
bool parse_shortcut(const std::string& text, std::vector<ShortcutStep>& out,
const KeyResolver& resolve) {
out.clear();
if (text.empty()) {
return false;
}
std::istringstream combos(text);
std::string combo;
while (std::getline(combos, combo, ',')) {
if (combo.empty()) {
return false;
}
ShortcutStep step;
bool haveKey = false;
std::istringstream parts(combo);
std::string part;
while (std::getline(parts, part, '+')) {
if (part.empty()) {
return false;
}
unsigned mod = modifier_bit(part);
if (mod != kModNone) {
step.modifiers |= mod;
continue;
}
int code = resolve ? resolve(part) : linux_key_code(part);
if (code < 0) {
return false; // unknown key name
}
step.key_name = part;
step.linux_key = code;
haveKey = true;
}
if (!haveKey) {
return false; // combo without an actual key
}
out.push_back(step);
}
return !out.empty();
}
} // namespace gkeymapper

68
shared/mapping.h Normal file
View File

@@ -0,0 +1,68 @@
// gKeyMapper - the mappings file format, shared by GUI and daemon.
//
// File format (one line per G key):
// # gKeyMapper mappings (G1..G9)
// G1=key:ctrl+c,ctrl+v
// G2=command:/usr/bin/firefox
// G3=disabled:
#ifndef GKEYMAPPER_MAPPING_H
#define GKEYMAPPER_MAPPING_H
#include <functional>
#include <string>
#include <vector>
namespace gkeymapper {
constexpr int kNumKeys = 9; // G1..G9
enum class ActionType { Disabled, Key, Command };
struct Mapping {
ActionType type = ActionType::Disabled;
std::string target;
};
// One entry per G key; index 0 -> G1.
struct MappingSet {
std::vector<Mapping> keys;
};
const char* action_type_token(ActionType type);
ActionType action_type_from_token(const std::string& token);
// Serialize to / parse from the text representation. parse_mappings always
// returns exactly kNumKeys entries (missing ones default to Disabled).
std::string format_mappings(const MappingSet& set);
MappingSet parse_mappings(const std::string& text);
bool load_mappings_file(const std::string& path, MappingSet& out);
bool save_mappings_file(const std::string& path, const MappingSet& set);
// Default config file: ~/.config/gKeyMapper/mappings.conf.
std::string default_config_path();
// One step of a "ctrl+c" shortcut, ready to be replayed via uinput.
struct ShortcutStep {
unsigned modifiers = 0; // ModifierBit mask
std::string key_name; // canonical key name (layout independent)
int linux_key = -1; // resolved input code, -1 if unresolved
};
// Resolves a canonical key name to an input code for the keyboard layout in
// use. The default (empty) resolver uses linux_key_code(), i.e. the
// layout-independent US key positions.
using KeyResolver = std::function<int(const std::string& name)>;
// Parse "ctrl+c,ctrl+v" into individual steps. Returns false if the text is
// empty or contains an unknown key name.
bool parse_shortcut(const std::string& text, std::vector<ShortcutStep>& out,
const KeyResolver& resolve = KeyResolver());
// Formats one step ("ctrl+shift+t") from a ModifierBit mask and a base key
// name. The modifier order is fixed: ctrl, alt, shift, super, altgr.
std::string format_shortcut_step(unsigned modifiers, const std::string& key_name);
} // namespace gkeymapper
#endif // GKEYMAPPER_MAPPING_H