Emacs style case conversions (#5898)

This PR introduces the following changes to rizin shell:

- Use Meta-L to lowercase the following word
- Use Meta-U to uppercase the following word
- Use Meta-C to capitalize the following word

The behavior of these actions is the same as in zsh.
This commit is contained in:
Ehab-24 2026-03-11 21:52:52 +05:00 committed by GitHub
parent 70fdd67278
commit 9f85d684a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 401 additions and 1 deletions

View file

@ -6,6 +6,7 @@
#include <rz_core.h>
#include <string.h>
#include <stdlib.h>
#include "i/private.h"
#if __WINDOWS__
#include <windows.h>
@ -1555,6 +1556,8 @@ RZ_API const char *rz_line_readline_cb(RZ_NONNULL RzLine *line, RzLineReadCallba
int prev_buflen = -1;
bool enable_yank_pop = false;
bool gcomp_is_rev = true;
RzEmacsModeModifyOpts em_opts;
rz_emacs_mode_modify_opts_reset(&em_opts);
RzCons *cons = rz_cons_singleton();
@ -1885,6 +1888,47 @@ RZ_API const char *rz_line_readline_cb(RZ_NONNULL RzLine *line, RzLineReadCallba
line->buffer.index = line->buffer.length;
}
break;
case 'C':
case 'c':
em_opts.op = EMACS_MODIFY_CAPITALIZE;
rz_emacs_mode_modify(&em_opts, line);
rz_emacs_mode_modify_opts_reset(&em_opts);
break;
case 'L':
case 'l':
em_opts.op = EMACS_MODIFY_TOLOWER;
rz_emacs_mode_modify(&em_opts, line);
rz_emacs_mode_modify_opts_reset(&em_opts);
break;
case 'U':
case 'u':
em_opts.op = EMACS_MODIFY_TOUPPER;
rz_emacs_mode_modify(&em_opts, line);
rz_emacs_mode_modify_opts_reset(&em_opts);
break;
case '-':
em_opts.move_cursor = !em_opts.move_cursor;
if (em_opts.word_count_provided) {
// word count must be provided after '-'
rz_emacs_mode_modify_opts_reset(&em_opts);
}
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9': {
const ut32 d = buf[0] - '0';
em_opts.word_count = em_opts.word_count <= (UT32_MAX - d) / 10
? em_opts.word_count * 10 + d
: UT32_MAX;
em_opts.word_count_provided = true;
} break;
case 63: // ^[? Meta-/
case 95: // ^[_ Meta-_
if (!line->gcomp && !line->hud && !line->sel_widget) {

322
librz/cons/emacs_mode.c Normal file
View file

@ -0,0 +1,322 @@
// SPDX-FileCopyrightText: 2026 Ehab-24 <ehabs1775@gmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#include <wctype.h>
#include "i/private.h"
static bool is_word_constituent(RzCodePoint cp) {
return iswalnum((wint_t)cp);
}
static char *unicode_mapping_append_to_buf(RZ_NONNULL const RzUnicodeCaseMapping *um, RZ_NONNULL char *p, RZ_NONNULL const char *end) {
rz_return_val_if_fail(um && p && end, NULL);
if (rz_unicode_case_mapping_is_empty(um)) {
const size_t len = rz_utf8_encode((ut8 *)p, um->key);
if (len > 0) {
if ((size_t)(end - p) < len) {
return NULL;
}
p += len;
}
} else {
for (int j = 0; j < 3; ++j) {
RzCodePoint v = um->val[j];
if (v == 0) {
continue;
}
const size_t len = rz_utf8_encode((ut8 *)p, v);
if (len < 1) {
continue;
}
if ((size_t)(end - p) < len) {
return NULL;
}
p += len;
}
}
return p;
}
/**
* \brief Convert a vector of RzUnicodeCaseMapping to an encoded UTF-8 string.
* NOTE: Empty mappings are encoded using their `key` insted of value(s).
*
* \param map RzVector <RzUnicodeCaseMapping>.
* \param maxlen The length of encoded string in bytes.
*
* \return Encoded UTF-8 (null-terminated) string.
*/
static RZ_OWN RZ_NULLABLE char *unicode_mapping_to_str(RZ_NONNULL RzUnicodeCaseMappings *map, size_t utf8_len) {
rz_return_val_if_fail(map, NULL);
char *utf8_buf = RZ_NEWS0(char, utf8_len + 1);
if (!utf8_buf) {
return NULL;
}
void *it;
char *ptr = utf8_buf;
char *const endptr = utf8_buf + utf8_len;
rz_vector_foreach (map, it) {
const RzUnicodeCaseMapping *um = (RzUnicodeCaseMapping *)it;
ptr = unicode_mapping_append_to_buf(um, ptr, endptr);
if (!ptr) {
RZ_FREE(utf8_buf);
return NULL;
}
}
return utf8_buf;
}
/**
* \brief Find index of the first (unicode) character in the following word in \p buffer.
*/
static ssize_t emacs_mode_find_word_start(RZ_NONNULL const RzCodePoints *buffer, ssize_t start) {
rz_return_val_if_fail(buffer && rz_vector_len(buffer), -1);
while (start < rz_vector_len(buffer)) {
const RzCodePoint *cp = rz_vector_index_ptr(buffer, start);
if (is_word_constituent(*cp)) {
break;
}
++start;
}
return start;
}
/**
* \brief Find index of the last (unicode) character in the following word in \p buffer.
*/
static ssize_t emacs_mode_find_word_end(RZ_NONNULL const RzCodePoints *buffer, ssize_t start) {
rz_return_val_if_fail(buffer && rz_vector_len(buffer), -1);
ssize_t end = RZ_MIN(start + 1, rz_vector_len(buffer));
while (end < rz_vector_len(buffer)) {
const RzCodePoint *cp = rz_vector_index_ptr(buffer, end);
if (!is_word_constituent(*cp)) {
break;
}
++end;
}
return end;
}
/**
* \brief Find the starting byte index of a unicode code point in \p buffer.
*
* \param buffer RzVector <RzUnicodePoint>.
* \param cp_index Index of the unicode code point.
*
* \return ssize_t -1 in case of failure.
*/
static ssize_t emacs_mode_find_byte_index(RZ_NONNULL const RzCodePoints *buffer, size_t cp_index) {
rz_return_val_if_fail(cp_index <= rz_vector_len(buffer), -1);
ssize_t bytei = 0;
for (size_t i = 0; i < cp_index; ++i) {
const RzCodePoint *cp = rz_vector_index_ptr(buffer, i);
const size_t len = rz_utf8_byte_length(*cp);
bytei += len ? len : 1;
}
return bytei;
}
/**
* \brief Capitalize the current or next word in \p buffer.
*
* \param buffer RzVector <RzCodePoint>.
* \param start Starting index of the word in \p buffer.
* \param end Ending index of the word in \p buffer.
* \param utf8_len Number of bytes between \p start (inclusive) and \p end (exclusive) in \p buffer.
*
* \return Capitalized word as UTF-8 encoded (null-terminated) string.
*/
static RZ_OWN RZ_NULLABLE char *emacs_mode_capitalize(RZ_NONNULL const RzCodePoints *buffer, ssize_t start, ssize_t end, size_t utf8_len) {
rz_return_val_if_fail(buffer && rz_vector_len(buffer) > 0 && utf8_len > 0 && start <= end && end <= rz_vector_len(buffer), NULL);
RzUnicodeCaseMappings *map = rz_vector_new(sizeof(RzUnicodeCaseMapping), NULL, NULL);
ssize_t i = start;
for (; i < end; ++i) {
const RzCodePoint *cp = rz_vector_index_ptr(buffer, i);
if (iswalpha((wint_t)(*cp))) {
RzUnicodeCaseMapping um = rz_unicode_code_point_find_upper(*cp);
rz_vector_push(map, &um);
break;
} else {
RzUnicodeCaseMapping um = rz_unicode_case_mapping_default(*cp);
rz_vector_push(map, &um);
}
}
for (++i; i < end; ++i) {
const RzCodePoint *cp = rz_vector_index_ptr(buffer, i);
RzUnicodeCaseMapping um = rz_unicode_code_point_find_lower(*cp);
rz_vector_push(map, &um);
}
char *utf8_buf = unicode_mapping_to_str(map, utf8_len);
rz_vector_free(map);
return utf8_buf;
}
/**
* \brief Lowercase the current or next word in \p buffer.
*
* \param buffer RzVector <RzCodePoint>.
* \param start Starting index of the word in \p buffer.
* \param end Ending index of the word in \p buffer.
* \param utf8_len Number of bytes between \p start (inclusive) and \p end (exclusive) in \p buffer.
*
* \return Lowercase word as UTF-8 encoded (null-terminated) string.
*/
static char *emacs_mode_tolower(RZ_NONNULL const RzCodePoints *buffer, ssize_t start, ssize_t end, size_t utf8_len) {
rz_return_val_if_fail(buffer && rz_vector_len(buffer) > 0 && utf8_len > 0 && start <= end && end <= rz_vector_len(buffer), NULL);
RzVector *map = rz_vector_new(sizeof(RzUnicodeCaseMapping), NULL, NULL);
for (ssize_t i = start; i < end; ++i) {
const RzCodePoint *cp = rz_vector_index_ptr(buffer, i);
RzUnicodeCaseMapping um = rz_unicode_code_point_find_lower(*cp);
rz_vector_push(map, &um);
}
char *utf8_buf = unicode_mapping_to_str(map, utf8_len);
rz_vector_free(map);
return utf8_buf;
}
/**
* \brief Uppercase the current or next word in \p buffer.
*
* \param buffer RzVector <RzCodePoint>.
* \param start Starting index of the word in \p buffer.
* \param end Ending index of the word in \p buffer.
* \param utf8_len Number of bytes between \p start (inclusive) and \p end (exclusive) in \p buffer.
*
* \return Uppercase word as UTF-8 encoded (null-terminated) string.
*/
static RZ_OWN RZ_NULLABLE char *emacs_mode_toupper(RZ_NONNULL const RzCodePoints *buffer, ssize_t start, ssize_t end, size_t utf8_len) {
rz_return_val_if_fail(buffer && rz_vector_len(buffer) > 0 && utf8_len > 0 && start <= end && end <= rz_vector_len(buffer), NULL);
RzVector *map = rz_vector_new(sizeof(RzUnicodeCaseMapping), NULL, NULL);
for (ssize_t i = start; i < end; ++i) {
const RzCodePoint *cp = rz_vector_index_ptr(buffer, i);
RzUnicodeCaseMapping um = rz_unicode_code_point_find_upper(*cp);
rz_vector_push(map, &um);
}
char *utf8_buf = unicode_mapping_to_str(map, utf8_len);
rz_vector_free(map);
return utf8_buf;
}
/**
* \brief Decode a UTF-8 encoded string as a vector of RzCodePoint into \p out.
*
* \param buf The UTF-8 encoded string.
* \param buflen Length of \p buf.
* \param out RzVector <RzCodePoint>.
*
* \return false on failure.
*/
static bool utf8_decode_buf(const ut8 *buf, size_t buflen, RZ_NONNULL RzCodePoints *out) {
rz_return_val_if_fail(out, false);
for (size_t i = 0; i < buflen;) {
RzCodePoint cp;
const size_t len = rz_utf8_decode(buf + i, buflen - i, &cp, false);
void *elem = rz_vector_push(out, &cp);
if (!elem) {
return false;
}
i += len ? len : 1;
}
return true;
}
static bool emacs_mode_modify_one(RzEmacsModeModifyOp op, RZ_NONNULL RzLine *line, RZ_NONNULL const RzCodePoints *cpbuffer, ssize_t start, size_t end) {
rz_return_val_if_fail(line && cpbuffer && start >= 0 && end >= 0, false);
const ssize_t start_byte = emacs_mode_find_byte_index(cpbuffer, start);
const ssize_t end_byte = emacs_mode_find_byte_index(cpbuffer, end);
if (start_byte == -1 || end_byte == -1) {
return false;
}
char *mword = NULL; ///< The modified word
switch (op) {
default:
rz_return_val_if_reached(false);
case EMACS_MODIFY_CAPITALIZE:
mword = emacs_mode_capitalize(cpbuffer, start, end, end_byte - start_byte);
break;
case EMACS_MODIFY_TOLOWER:
mword = emacs_mode_tolower(cpbuffer, start, end, end_byte - start_byte);
break;
case EMACS_MODIFY_TOUPPER:
mword = emacs_mode_toupper(cpbuffer, start, end, end_byte - start_byte);
break;
}
if (!mword) {
return false;
}
char *p = line->buffer.data + start_byte;
memcpy(p, mword, end_byte - start_byte);
RZ_FREE(mword);
return true;
}
/**
* \brief Capitalizes/lowercases/uppercases the current or next word(s).
* Note: Optionally sets the cursor position to the end of the last word.
*
* \param opts The modify options
* \param line RzLine
*
* \return false on failure
*/
RZ_IPI bool rz_emacs_mode_modify(RZ_NONNULL RzEmacsModeModifyOpts *opts, RZ_NONNULL RzLine *line) {
rz_return_val_if_fail(line && opts, false);
if (line->buffer.length < 1) {
return true;
}
RzCodePoints *cpbuffer = rz_vector_new(sizeof(RzCodePoint), NULL, NULL);
const ut8 *line_buf = (ut8 *)line->buffer.data;
const bool decoded = utf8_decode_buf(line_buf, line->buffer.length, cpbuffer);
if (!decoded || rz_vector_len(cpbuffer) < 1) {
goto cleanup_and_exit;
}
const size_t word_count = opts->word_count_provided ? opts->word_count : 1;
const size_t utf8_index = rz_utf8_strnlen(line_buf, line->buffer.index);
ssize_t start = 0, end = 0; ///< index of first/last unicode code point in the current word
start = emacs_mode_find_word_start(cpbuffer, utf8_index);
end = emacs_mode_find_word_end(cpbuffer, start);
for (size_t wi = 0; wi < word_count && start < end; ++wi) {
if (start == -1 || end == -1) {
goto cleanup_and_exit;
}
const bool failed = !emacs_mode_modify_one(opts->op, line, cpbuffer, start, end);
if (failed) {
goto cleanup_and_exit;
}
if (opts->move_cursor) {
line->buffer.index = emacs_mode_find_byte_index(cpbuffer, end);
}
start = emacs_mode_find_word_start(cpbuffer, end);
end = emacs_mode_find_word_end(cpbuffer, start);
}
rz_vector_free(cpbuffer);
return true;
cleanup_and_exit:
rz_vector_free(cpbuffer);
return false;
}
/**
* \brief Reset \p opts to their default state.
*
* \param opts The modify options.
*/
RZ_IPI void rz_emacs_mode_modify_opts_reset(RZ_NONNULL RzEmacsModeModifyOpts *opts) {
rz_return_if_fail(opts);
opts->move_cursor = true;
opts->word_count_provided = false;
opts->word_count = 0;
}

30
librz/cons/i/private.h Normal file
View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2026 Ehab-24 <ehabs1775@gmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#ifndef RZ_CONS_PRIVATE_H_
#define RZ_CONS_PRIVATE_H_
#include <rz_util.h>
#include <rz_types.h>
#include <rz_cons.h>
typedef enum {
EMACS_MODIFY_CAPITALIZE,
EMACS_MODIFY_TOLOWER,
EMACS_MODIFY_TOUPPER
} RzEmacsModeModifyOp;
typedef struct {
RzEmacsModeModifyOp op;
bool move_cursor; ///< if true, cursor is moved to the end of the last word.
bool word_count_provided; ///< if true \p word_count is used, else a single word is modified.
size_t word_count; ///< number of words to modify.
} RzEmacsModeModifyOpts;
typedef RzVector /*<RzCodePoint>*/ RzCodePoints;
typedef RzVector /*<RzUnicodeCaseMapping>*/ RzUnicodeCaseMappings;
RZ_IPI void rz_emacs_mode_modify_opts_reset(RZ_NONNULL RzEmacsModeModifyOpts *opts);
RZ_IPI bool rz_emacs_mode_modify(RZ_NONNULL RzEmacsModeModifyOpts *opts, RZ_NONNULL RzLine *line);
#endif

View file

@ -17,7 +17,8 @@ rz_cons_sources = [
'prompt.c',
'cpipe.c',
'rgb.c',
'cutf8.c'
'cutf8.c',
'emacs_mode.c'
]
rz_cons = library('rz_cons', rz_cons_sources,

View file

@ -8,6 +8,7 @@
#include <rz_project.h>
#include <rz_flirt.h>
#include <rz_socket.h>
#include <locale.h>
static bool is_valid_gdb_file(RzCoreFile *fh) {
RzIODesc *d = fh && fh->core ? rz_io_desc_get(fh->core->io, fh->fd) : NULL;
@ -444,6 +445,8 @@ RZ_API int rz_main_rizin(int argc, const char **argv) {
RzList *prefiles = rz_list_new();
RzCmdStateOutput state = { 0 };
setlocale(LC_CTYPE, "");
#define LISTS_FREE() \
{ \
rz_list_free(cmds); \