From 7b12bdcf546a26666868b6a6ac76deeffa732d61 Mon Sep 17 00:00:00 2001 From: Ivan Chikish Date: Mon, 23 Dec 2024 13:08:08 +0300 Subject: [PATCH] Implement push, pop(_all) bind commands; user bindings; update README.md These commands may be useful to execute at user session startup. User bindings are loaded by explicit calling `keyd reload` from user. User bindings are located at `~/.config/keyd/bindings.conf`. --- README.md | 120 ++++++++++++++---------------- data/keyd-application-mapper.1.gz | Bin 1644 -> 1643 bytes data/keyd.1.gz | Bin 10092 -> 10053 bytes docs/CHANGELOG.md | 83 --------------------- docs/keyd.scdoc | 16 ++-- src/config.cpp | 35 ++++++--- src/config.h | 6 +- src/daemon.cpp | 57 +++++++++++--- src/keyboard.cpp | 35 ++++++--- src/keyboard.h | 4 +- src/macro.cpp | 6 +- t/test-io.cpp | 2 +- 12 files changed, 166 insertions(+), 198 deletions(-) diff --git a/README.md b/README.md index 91b1bff..6e361ef 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,4 @@ -[![Kofi](https://badgen.net/badge/icon/kofi?icon=kofi&label)](https://ko-fi.com/rvaiya) - -# Impetus - -[![Packaging status](https://repology.org/badge/tiny-repos/keyd.svg)](https://repology.org/project/keyd/versions) +# keyd++ Linux lacks a good key remapping solution. In order to achieve satisfactory results a medley of tools need to be employed (e.g xcape, xmodmap) with the end @@ -10,20 +6,12 @@ result often being tethered to a specified environment (X11). keyd attempts to solve this problem by providing a flexible system wide daemon which remaps keys using kernel level input primitives (evdev, uinput). -# Note on v2 - -The config format has undergone several iterations since the first -release. For those migrating their configs from v1 it is best -to reread the man page. - -See also: [changelog](docs/CHANGELOG.md). - # Goals - - Speed (a hand tuned input loop written in C that takes <<1ms) + - Speed (event loop that takes <<1ms for input event) - Simplicity (a [config format](#sample-config) that is intuitive) - - Consistency (modifiers that [play nicely with layers](https://github.com/rvaiya/keyd/blob/6dc2d5c4ea76802fd192b143bdd53b1787fd6deb/docs/keyd.scdoc#L128) by default) - - Modularity (a UNIXy core extensible through the use of an [IPC](https://github.com/rvaiya/keyd/blob/90973686723522c2e44d8e90bb3508a6da625a20/docs/keyd.scdoc#L391) mechanism) + - Consistency (modifiers that [play nicely with layers](docs/keyd.scdoc#L128) by default) + - Modularity (a UNIXy core extensible through the use of an [IPC](docs/keyd.scdoc#L391) mechanism) # Features @@ -33,7 +21,7 @@ as well as some which are unique to keyd. Some of the more interesting ones include: -- Layers (with support for [hybrid modifiers](https://github.com/rvaiya/keyd/blob/6dc2d5c4ea76802fd192b143bdd53b1787fd6deb/docs/keyd.scdoc#L128)). +- Layers (with support for [hybrid modifiers](docs/keyd.scdoc#L128)). - Key overloading (different behaviour on tap/hold). - Keyboard specific configuration. - Instantaneous remapping (no more flashing :)). @@ -42,6 +30,23 @@ Some of the more interesting ones include: - First class support for modifier overloading. - Unicode support. +keyd++ has specific features at the moment: + +- Virtually unlimited sizes/counts (keyd has had many hardcoded limitations). +- Layer indicator with keyboard led of choice (keyd is somewhat broken). +- **Macro** can now do `type(Hello world)` without worrying about spaces. +- **Macro** can now execute `cmd(gnome-terminal)` and it should **just work**. +- Allow using `ctrl` as an alias for `control` (my personal whim). +- Wildcard for mice `m:` that excludes problematic abs ptr devices(`a:`). +- More flexible text parsing (in progress, eg. 'a+b' now equals 'b+a'). +- Memory optimizations (sometimes only 1/5 of what keyd has had). +- Performance optimizations, eg. events from ungrabbed device are ignored. +- Some security improvement: privileged commands shall be in /etc/keyd/ conf. +- Bindings coming from ex. `keyd-application-mapper` inherit uid+gid+environ. +- `keyd reload` from user loads user binds from `~/.config/keyd/bindings.conf`. +- New commands for config control: push, pop, pop_all. Can unload user binds. +- Convenience and safety coming from C++20. Sorry for longer compilation. + ### keyd is for people who: - Would like to experiment with custom [layers](https://docs.qmk.fm/feature_layers) (i.e custom shift keys) @@ -59,7 +64,7 @@ Some of the more interesting ones include: # Dependencies - - Your favourite C compiler + - C++20 compiler starting from clang++-14 or g++-11 - Linux kernel headers (already present on most systems) ## Optional @@ -70,15 +75,22 @@ Some of the more interesting ones include: # Installation -*Note:* master serves as the development branch, things may occasionally break -between releases. Releases are [tagged](https://github.com/rvaiya/keyd/tags), and should be considered stable. +*Note:* master serves as the development branch, things may occasionally break. ## From Source - git clone https://github.com/rvaiya/keyd - cd keyd - make && sudo make install - sudo systemctl enable --now keyd +```bash +# Install dependencies (if necessary) +sudo apt install build-essentials git +# Clone with git clone (.) or download sources manually to keyd directory +cd keyd +# Specify your favourite compiler (optional) +export CXX=clang++-18 +# First time install +make && sudo make install && sudo systemctl enable --now keyd +# Second time install (update, contains **example** flags for statically linking libstdc++) +CXX=clang++-18 CXXFLAGS='-static-libgcc -static-libstdc++' make && sudo make install && sudo systemctl daemon-reload && sudo systemctl restart keyd +``` # Quickstart @@ -119,6 +131,24 @@ Some mice (e.g the Logitech MX Master) are capable of emitting keys and are consequently matched by the wildcard id. It may be necessary to explicitly blacklist these. +## User Specific Remapping (experimental) + +- Add yourself to the keyd group: + + `usermod -aG keyd ` + +- Create `~/.config/keyd/bindings.conf`: + +E.G + meta.t = macro(cmd(gnome-terminal)) + +- Execute `keyd reload` without sudo (possibly at startup) + +- Environment variables whel launching `keyd reload` will be used. + +- `/root/keyd/bindings.conf` may be read at `sudo keyd reload`. + + ## Application Specific Remapping (experimental) - Add yourself to the keyd group: @@ -161,46 +191,6 @@ Third party packages for the some distributions also exist. If you wish to add yours to the list please file a PR. These are kindly maintained by community members, no personal responsibility is taken for them. -### Alpine Linux - -[keyd](https://pkgs.alpinelinux.org/packages?name=keyd) package maintained by [@jirutka](https://github.com/jirutka). - -### Arch - -[Arch Linux](https://archlinux.org/packages/extra/x86_64/keyd/) package maintained by Arch packagers. - -### Debian - -Experimental `keyd` and `keyd-application-mapper` packages can be found in the -CI build artifacts of the [work-in-progress Debian package -repository](https://salsa.debian.org/rhansen/keyd): - - * [amd64 (64-bit)](https://salsa.debian.org/rhansen/keyd/-/jobs/artifacts/debian/latest/browse/debian/output?job=build) - * [i386 (32-bit)](https://salsa.debian.org/rhansen/keyd/-/jobs/artifacts/debian/latest/browse/debian/output?job=build%20i386) - -Any Debian Developer who is willing to review the debianization effort and -sponsor its upload is encouraged to contact -[@rhansen](https://github.com/rhansen) (also see the [Debian ITP -bug](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1060023)). - -### Fedora - -[COPR](https://copr.fedorainfracloud.org/coprs/alternateved/keyd/) package maintained by [@alternateved](https://github.com/alternateved). - -### openSUSE -[opensuse](https://software.opensuse.org//download.html?project=hardware&package=keyd) package maintained by [@bubbleguuum](https://github.com/bubbleguuum). - -Easy install with `sudo zypper in keyd`. - -### Ubuntu - -Experimental `keyd` and `keyd-application-mapper` packages can be found in the -[`ppa:keyd-team/ppa` -archive](https://launchpad.net/~keyd-team/+archive/ubuntu/ppa). - -If you wish to help maintain this PPA, please contact -[@rhansen](https://github.com/rhansen). - # Sample Config [ids] @@ -218,7 +208,7 @@ If you wish to help maintain this PPA, please contact f = / ... -# Recommended config +# Example config Many users will probably not be interested in taking full advantage of keyd. For those who seek simple quality of life improvements I can recommend the diff --git a/data/keyd-application-mapper.1.gz b/data/keyd-application-mapper.1.gz index bc2c9be543d70ee4e644a4673ef2db8a00c4697a..38f36cafbc610811975012bf72265e34a0795022 100644 GIT binary patch delta 699 zcmV;s0!01n4C@S#=6`;BkmVn;eDNb)OyAA;`H6!xBCqjC_#WtPty{V?ZRze1bI}DV z)~c;_7_2QmUA+IWm@oK!S)Dy$bXi@}H9n6@Jlzau-^>=TFXtbw=I<|fX$tne*+e)s z#TqCS_*ho+KgRj;8G^1%J!bo3nCw^Bu4+pXk2)YFnnEWpWq-B;Y}3{$2)#vUY6#`d z#5LX;6^k{tP+?U-Xs&?`(?*wKg%ig0$#qnymh;`-lB-B7zze#NN{~DU9r?q9z$t8W zVJcI=Nw^I?!ZLG=gJIUzt&}C-S@bc}Bov9g7s{@jlKT%oUC%E~qjnv$aZ5 zSI$~@$FZO^n|~KVaa(2i<$}J+zPNsg%~AVTH(B<@OZ|D{bqI`d^6K&XDc+aQ^7NbK zm4OARmY>(!YX&!1V(QtsYc+@dcMKtc5CGRN6T|TI>V_Wud`0g#3*3Bta%AhHBfr0l zCL-AW_=6*950`-KNFHYyFG!Xc@IE{NZ8%|DWev*4*MHK}gp6Y+Q%+npyzMY(j`!@W zuOh4+^*LapdvHSy^6DO@tb(x+eP4rI)1~GTU+G7o?%`9bP=OiHV2c!|E>Glhqjuyr zTv*E5ok)N}v2?BZ3>Q1g7HwH5kCr(+W}x%~Bual)EMF%K`MPdV-@hND(#qo-#)l&| zxq*+}Uw?WdK|^M7R8HRaT2FP4fzzGZZk1x?163li=WB-l=YK27TNC4s>rvT=V!NU5 z>Gde8)v$0a3yX(Kaah8s>kvoB`{NVBw?-A-9nRZeN&woZg84t<&BfZg#&iuIVBp?a zIrNWbWV8PA=)XgYk6MQ5O{rlScnh}<$;!X{Xv>+9vS!F$AWlH9*4RqBbjRuQFv h>mZ~L*bf@`)S(8+HB3P2vxo!S0tk@*rRx(7006?BVch@# delta 700 zcmV;t0z>`l4D1Y$=6{|ZWci0IU;IcH(|0p|e&QgF$ZI?jz6ZKn>z3|JTe>^MTy%ko zwQ6e}25XB?7wI0avc?_<$SlddFRou=bJYITO_qJ}Qh(lf9Rj1Ayn6h8iudKSJpE>Q zWne+7<>$5bn!yd0n0j{ZTFs&V9YaVU1i-J|loMACZ#xW{<2^g; zs|agHeGb^@9^6oayt;=et6(fd-`615bg8+-SNc(?d-&8URA2@)*doQL%MGYM;B=?9Tcud}K$S@B`I_PX`QJ+N*2K8udQ>)|*ly^1 zdOeD2H7s1q!s6jl9F}nEI>gcO{`iFOtx<({hx0a=5`Z?UVE&JIbFucWF1e_XBDc(zFejGi`g-|n@E-A;B=@gMm3ktsRYYmv iItb|l_JamKb*Mpd4HKi&Z?lI3+yV&WcI*HX4FCZ4l4gql diff --git a/data/keyd.1.gz b/data/keyd.1.gz index 6fb6c563ce31e5598e8368a3f7abff1d2c85df75..df1750dd213131846e84f891d2108db3f64df2c5 100644 GIT binary patch literal 10053 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 b$c8CI9bee%vazJL#Ax_`m__46s&N1S4#lAL literal 10092 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 O=>Gr!`Y0bPasU8 [...]* +*bind reset|push|pop|pop_all [...]* Apply the supplied bindings. See _Bindings_ for details. *reload* - Reload config files. + Reload config files. Optionally loads bindings from $HOME/.config/keyd/bindings.conf *list-keys* List valid key names. @@ -797,8 +797,9 @@ The _bind_ command accepts one or more _bindings_, each of which must have the f Where __ is the name of an (existing) layer in which the key is to be bound. As a special case, the string "reset" may be used in place of a binding, in -which case the current keymap will revert to its original state (all +which case the current keymap will revert to its last pushed state (all dynamically applied bindings will be dropped). +Commands "push" and "pop|pop_all" maintain additional layers of states. Examples: @@ -970,10 +971,5 @@ Disables the esc and end keys. # AUTHOR -Written by Raheman Vaiya (2017-). - -# BUGS - -Please file any bugs or feature requests at the following url: - - +Written by Raheman Vaiya (2017-) in C. +Port to C++ in progress by Nekotekina. diff --git a/src/config.cpp b/src/config.cpp index e57f035..1f1bd6a 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -326,9 +326,6 @@ static int set_layer_entry(const struct config *config, static std::string layer_sorted_name(std::string_view name) { -#ifdef __cpp_lib_constexpr_vector - constinit -#endif static std::vector arr; arr.assign(split_char<'+'>(name), split_str<'+'>()); for (auto& name : arr) { @@ -381,7 +378,7 @@ static int new_layer(std::string_view s, std::string_view name, struct config *c for (auto layername : split_char<'+'>(name)) { int idx = config_access_layer(config, layername, true); if (idx < 0) { - err("%s is not a valid layer", std::string(layername).c_str()); + err("%.*s is not a valid layer", (int)layername.size(), layername.data()); return -1; } @@ -401,7 +398,7 @@ static int new_layer(std::string_view s, std::string_view name, struct config *c layer->mods = mods; } else { if (!type.empty()) - warn("\"%s\" is not a valid layer type, ignoring\n", std::string(type).c_str()); + warn("\"%.*s\" is not a valid layer type, ignoring\n", (int)type.size(), type.data()); layer->type = LT_NORMAL; layer->mods = 0; @@ -538,7 +535,7 @@ static int parse_macro_expression(std::string_view s, macro& macro, struct confi s.remove_suffix(1); s.remove_prefix(6); } else if (parse_key_sequence(s, &code, &mods) && utf8_strlen(s) != 1) { - err("Invalid macro: %s\n", std::string(s).c_str()); + err("Invalid macro: %.*s\n", (int)s.size(), s.data()); return -1; } @@ -905,7 +902,7 @@ static int config_parse_string(struct config *config, char *content) entry += ent->key; entry += " = "; entry += ent->val; - if (config_add_entry(config, entry.c_str()) < 0) + if (config_add_entry(config, entry) < 0) keyd_log("\tr{ERROR:} line m{%zd}: %s\n", ent->lnum, errstr); } } @@ -1011,14 +1008,15 @@ int config_get_layer_index(const struct config *config, std::string_view name) * Adds a binding of the form [.] = * to the given config. Returns layer index that was modified. */ -int config_add_entry(struct config *config, const char *exp) +int config_add_entry(struct config* config, std::string_view exp) { char *keyname, *descstr, *dot, *paren, *s; const char *layername = "main"; struct descriptor d; struct layer *layer; - std::string buf = exp; + static std::string buf; + buf.assign(exp); s = buf.data(); dot = strchr(s, '.'); @@ -1048,6 +1046,25 @@ int config_add_entry(struct config *config, const char *exp) return idx; } +const char* env_pack::getenv(std::string_view name) +{ + if (!env) + return nullptr; + for (size_t i = 0;; i++){ + const char* ptr = env[i]; + if (!ptr) + return nullptr; + // TODO: is this safe? + if (!strncmp(ptr, name.data(), name.size())) { + ptr += name.size(); + if (*ptr == '=') + return ptr + 1; + if (*ptr == 0) + return nullptr; + } + } +} + config_backup::config_backup(const struct config& cfg) : descriptor_count(cfg.descriptors.size()) , macro_count(cfg.macros.size()) diff --git a/src/config.h b/src/config.h index 830ba4b..e4f661c 100644 --- a/src/config.h +++ b/src/config.h @@ -106,7 +106,7 @@ enum class layer_type_e : signed char { using enum layer_type_e; struct layer { - mutable std::string name_buf; + std::string name_buf; enum layer_type_e type; bool modified = false; // Modified by kbd_eval @@ -132,6 +132,8 @@ struct env_pack { std::unique_ptr env; uid_t uid; gid_t gid; + + const char* getenv(std::string_view); }; struct ucmd { @@ -202,7 +204,7 @@ struct config_backup { }; int config_parse(struct config *config, const char *path); -int config_add_entry(struct config *config, const char *exp); +int config_add_entry(struct config *config, std::string_view); int config_check_match(struct config *config, const char *id, uint8_t flags); diff --git a/src/daemon.cpp b/src/daemon.cpp index 95959d7..1ebd00b 100644 --- a/src/daemon.cpp +++ b/src/daemon.cpp @@ -1,4 +1,5 @@ #include "keyd.h" +#include "log.h" #include #include #include @@ -164,7 +165,7 @@ static void restore_leds() } } -static void on_layer_change(const struct keyboard *kbd, const struct layer *layer, uint8_t state) +static void on_layer_change(const struct keyboard *kbd, struct layer *layer, uint8_t state) { if (kbd->config.layer_indicator <= LED_MAX) { activate_leds(kbd); @@ -212,7 +213,7 @@ static void load_configs() .on_layer_change = on_layer_change, }; kbd = new_keyboard(std::move(kbd)); - kbd->original_config.reserve(1); + kbd->original_config.reserve(2); kbd->original_config.emplace_back(kbd->config); ent->kbd = std::move(kbd); @@ -283,7 +284,7 @@ static void manage_device(struct device *dev) } } -static void reload() +static void reload(std::shared_ptr env) { restore_leds(); configs.reset(); @@ -293,6 +294,39 @@ static void reload() manage_device(&device_table[i]); clear_vkbd(); + + if (env) { + // Load user bindings (may be not loaded when executed as root) + static std::string buf; + if (env->uid == 0) + buf.assign("/root/.config/"); + else if (auto v = env->getenv("XDG_CONFIG_HOME")) + buf.assign(v) += "/"; + else if (auto v = env->getenv("HOME")) + buf.assign(v) += "/.config/"; + else + buf.clear(); + buf += "keyd/bindings.conf"; + std::ifstream file(buf, std::ios::binary); + if (!file.is_open()) { + keyd_log("Unable to open %s\n", buf.c_str()); + return; + } + buf.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + + for (auto ent = configs.get(); ent; ent = ent->next.get()) { + ent->kbd->config.cfg_use_uid = env->uid; + ent->kbd->config.cfg_use_gid = env->gid; + ent->kbd->config.env = env; + for (auto str : split_char<'\n'>(buf)) { + if (str.empty()) + continue; + if (!kbd_eval(ent->kbd.get(), str)) + keyd_log("Invalid binding: %.*s\n", (int)str.size(), str.data()); + } + ent->kbd->original_config.emplace_back(ent->kbd->config); + } + } } static void send_success(int con) @@ -386,7 +420,7 @@ static int input(char *buf, [[maybe_unused]] size_t sz, uint32_t timeout) return 0; } -static bool handle_message(int con, struct config* config) +static bool handle_message(int con, struct config* config, std::shared_ptr env) { struct ipc_message msg; if (!xread(con, &msg, sizeof(msg))) { @@ -429,7 +463,7 @@ static bool handle_message(int con, struct config* config) send_success(con); break; case IPC_RELOAD: - reload(); + reload(std::move(env)); send_success(con); break; case IPC_LAYER_LISTEN: @@ -449,8 +483,7 @@ static bool handle_message(int con, struct config* config) ent->kbd->config.cfg_use_uid = config->cfg_use_uid; ent->kbd->config.cfg_use_gid = config->cfg_use_gid; ent->kbd->config.env = config->env; - if (!kbd_eval(ent->kbd.get(), msg.data)) - success = 1; + success |= kbd_eval(ent->kbd.get(), msg.data); } if (success) @@ -478,6 +511,9 @@ static void handle_client(int con) ::config ephemeral_config; ephemeral_config.cfg_use_gid = cred.gid; ephemeral_config.cfg_use_uid = cred.uid; + static std::shared_ptr prev = nullptr; + if (!prev || prev->uid != cred.uid || prev->gid != cred.gid) + prev.reset(); if (getuid() < 1000) { // Copy initial environment variables from caller process @@ -485,8 +521,7 @@ static void handle_client(int con) if (envf.is_open()) { std::vector buf; buf.assign(std::istreambuf_iterator(envf), std::istreambuf_iterator()); - static std::shared_ptr prev = nullptr; - if (prev && prev->buf == buf && prev->uid == cred.uid && prev->gid == cred.gid) { + if (prev && prev->buf == buf) { // Share previous environment variables ephemeral_config.env = prev; } else { @@ -507,7 +542,7 @@ static void handle_client(int con) } size_t msg_count = 0; - while (handle_message(con, &ephemeral_config)) { + while (handle_message(con, &ephemeral_config, prev ? prev : std::make_shared())) { msg_count++; } dbg2("%zu messages processed", msg_count); @@ -675,7 +710,7 @@ int run_daemon(int, char *[]) evloop_add_fd(ipcfd); - reload(); + reload(std::make_shared()); atexit(cleanup); diff --git a/src/keyboard.cpp b/src/keyboard.cpp index e4c0626..5237934 100644 --- a/src/keyboard.cpp +++ b/src/keyboard.cpp @@ -245,9 +245,7 @@ static void lookup_descriptor(struct keyboard *kbd, uint8_t code, static void deactivate_layer(struct keyboard *kbd, int idx) { ::layer& layer = kbd->config.layers[idx]; - layer.name_buf.back() = 0; - dbg("Deactivating layer %s", layer.name_buf.c_str() + 1); - layer.name_buf.back() = '\n'; + dbg("Deactivating layer %.*s", (int)layer.name_buf.size() - 2, layer.name_buf.c_str() + 1); assert(kbd->layer_state[idx].active > 0); kbd->layer_state[idx].active--; @@ -263,9 +261,7 @@ static void deactivate_layer(struct keyboard *kbd, int idx) static void activate_layer(struct keyboard *kbd, uint8_t code, int idx) { ::layer& layer = kbd->config.layers[idx]; - layer.name_buf.back() = 0; - dbg("Activating layer %s", layer.name_buf.c_str() + 1); - layer.name_buf.back() = '\n'; + dbg("Activating layer %.*s", (int)layer.name_buf.size() - 2, layer.name_buf.c_str() + 1); struct cache_entry *ce; kbd->layer_state[idx].activation_time = get_time(); @@ -402,7 +398,7 @@ void execute_command(ucmd& cmd) dup2(fd, 1); dup2(fd, 2); - if (auto env = cmd.env.get()) + if (auto env = cmd.env.get(); env && env->env) execle("/bin/sh", "/bin/sh", "-c", cmd.cmd.c_str(), nullptr, env->env.get()); else execl("/bin/sh", "/bin/sh", "-c", cmd.cmd.c_str(), nullptr); @@ -1214,20 +1210,35 @@ long kbd_process_events(struct keyboard *kbd, const struct key_event *events, si return timeout; } -int kbd_eval(struct keyboard *kbd, const char *exp) +bool kbd_eval(struct keyboard* kbd, std::string_view exp) { - if (!strcmp(exp, "reset")) { + if (exp.empty()) + return true; + if (exp == "reset") { kbd->original_config.back().restore(kbd->config); kbd->layer_state.resize(kbd->config.layers.size()); - return 0; + return true; + } else if (exp == "push") { + kbd->original_config.emplace_back(kbd->config); + return true; + } else if (exp == "pop") { + // Don't allow removing first "backup" + if (kbd->original_config.size() <= 1) + return false; + kbd->original_config.pop_back(); + return true; + } else if (exp == "pop_all") { + while (kbd->original_config.size() > 1) + kbd->original_config.pop_back(); + return true; } else { if (int idx = config_add_entry(&kbd->config, exp); idx >= 0) { kbd->layer_state.resize(kbd->config.layers.size()); kbd->config.layers[idx].modified = true; kbd->config.layers[idx].keymap.sort(); - return 0; + return true; } } - return -1; + return false; } diff --git a/src/keyboard.h b/src/keyboard.h index d9828ae..ef86153 100644 --- a/src/keyboard.h +++ b/src/keyboard.h @@ -33,7 +33,7 @@ struct key_event { struct output { void (*send_key) (uint8_t code, uint8_t state); - void (*on_layer_change) (const struct keyboard *kbd, const struct layer *layer, uint8_t active); + void (*on_layer_change) (const struct keyboard *kbd, struct layer *layer, uint8_t active); }; enum class chord_state_e : signed char { @@ -148,7 +148,7 @@ struct keyboard { std::unique_ptr new_keyboard(std::unique_ptr); long kbd_process_events(struct keyboard *kbd, const struct key_event *events, size_t n); -int kbd_eval(struct keyboard *kbd, const char *exp); +bool kbd_eval(struct keyboard *kbd, std::string_view); void kbd_reset(struct keyboard *kbd); #endif diff --git a/src/macro.cpp b/src/macro.cpp index 3e24743..e2cf053 100644 --- a/src/macro.cpp +++ b/src/macro.cpp @@ -89,7 +89,7 @@ int macro_parse(std::string_view s, macro& macro, struct config* config) tok.remove_prefix(chrsz); } if (!tok.empty()) { - err("invalid macro text found: %s", std::string(tok).c_str()); + err("invalid macro text found: %.*s", (int)tok.size(), tok.data()); return -1; } } @@ -112,7 +112,7 @@ int macro_parse(std::string_view s, macro& macro, struct config* config) else if (!parse_key_sequence(key, &code, &mods)) ADD_ENTRY(MACRO_HOLD, code); else { - err("%s is not a valid key", std::string(key).c_str()); + err("%.*s is not a valid key", (int)key.size(), key.data()); return -1; } } @@ -153,7 +153,7 @@ int macro_parse(std::string_view s, macro& macro, struct config* config) } } - err("%s is not a valid key sequence", std::string(tok).c_str()); + err("%.*s is not a valid key sequence", (int)tok.size(), tok.data()); return -1; } diff --git a/t/test-io.cpp b/t/test-io.cpp index d4ecbc0..b712fc4 100644 --- a/t/test-io.cpp +++ b/t/test-io.cpp @@ -234,7 +234,7 @@ uint64_t run_test(struct keyboard *kbd, const char *path) return time; } -static void on_layer_change(const struct keyboard *kbd, const struct layer *layer, uint8_t active) +static void on_layer_change(const struct keyboard *kbd, struct layer *layer, uint8_t active) { }