Deamon management
This commit is contained in:
134
shared/keymap.cpp
Normal file
134
shared/keymap.cpp
Normal 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
51
shared/keymap.h
Normal 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
110
shared/keys.cpp
Normal 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
41
shared/keys.h
Normal 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
193
shared/mapping.cpp
Normal 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
68
shared/mapping.h
Normal 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
|
||||
Reference in New Issue
Block a user