From b38d73ee3abd9aff8a23b514003b0748461feb7f Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 6 Aug 2026 10:02:51 +0200 Subject: [PATCH 1/5] implement case switching as key action --- config.def.h | 6 ++--- main.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/config.def.h b/config.def.h index a540201fb..8c13a57d0 100644 --- a/config.def.h +++ b/config.def.h @@ -160,9 +160,9 @@ static const KeyBinding bindings_operators[] = { { "'", ACTION(MARK) }, { "c", ACTION(OPERATOR_CHANGE) }, { "d", ACTION(OPERATOR_DELETE) }, - { "g~", ALIAS("|tr '[:lower:][:upper:]' '[:upper:][:lower:]'") }, - { "gu", ALIAS("|awk '{printf \"%s\", tolower($0)}'")}, - { "gU", ALIAS("|awk '{printf \"%s\", toupper($0)}'")}, + { "g~", ACTION(SELECTIONS_CASE_TOGGLE) }, + { "gu", ACTION(SELECTIONS_CASE_TOLOWER) }, + { "gU", ACTION(SELECTIONS_CASE_TOUPPER) }, { "p", ACTION(PUT_AFTER) }, { "P", ACTION(PUT_BEFORE) }, { "y", ACTION(OPERATOR_YANK) }, diff --git a/main.c b/main.c index 1a23c10ce..a780cf052 100644 --- a/main.c +++ b/main.c @@ -118,6 +118,9 @@ static Vis vis[1]; X(ka_selections_align, SELECTIONS_ALIGN, 0, "vis-selections-align", "Try to align all selections on the same column") \ X(ka_selections_align_indent, SELECTIONS_ALIGN_INDENT_LEFT, .i = -1, "vis-selections-align-indent-left", "Left-align all selections by inserting spaces") \ X(ka_selections_align_indent, SELECTIONS_ALIGN_INDENT_RIGHT, .i = +1, "vis-selections-align-indent-right", "Right-align all selections by inserting spaces") \ + X(ka_selections_case, SELECTIONS_CASE_TOUPPER, .i = +1, "vis-selections-case-toupper", "") \ + X(ka_selections_case, SELECTIONS_CASE_TOGGLE, .i = 0, "vis-selections-case-toggle", "") \ + X(ka_selections_case, SELECTIONS_CASE_TOLOWER, .i = -1, "vis-selections-case-tolower", "") \ X(ka_selections_clear, SELECTIONS_REMOVE_ALL, 0, "vis-selections-remove-all", "Remove all but the primary selection") \ X(ka_selections_complement, SELECTIONS_COMPLEMENT, 0, "vis-selections-complement", "Complement selections") \ X(ka_selections_intersect, SELECTIONS_INTERSECT, 0, "vis-selections-intersect", "Intersect with selections from mark") \ @@ -350,6 +353,76 @@ static KEY_ACTION_FN(ka_selections_align_indent) return keys; } +static KEY_ACTION_FN(ka_selections_case) +{ + Text *txt = vis_text(vis); + View *view = vis_view(vis); + char *buf; + wchar_t *wcs; + for (Selection *s = view_selections(view), *next; s; s = next) { + next = view_selections_next(s); + Filerange sel = view_selections_get(s); + if (!text_range_valid(sel)) + continue; + + buf = text_bytes_alloc0(txt, sel.start, text_range_size(sel)); + if (!buf) + return keys; + + size_t mbslen = mbstowcs(NULL, buf, 0); + if (mbslen == (size_t) -1) { + goto err_free_buf; + } + + wcs = calloc(mbslen + 1, sizeof(*wcs)); + if (wcs == NULL) { + goto err_free_buf; + } + + if (mbstowcs(wcs, buf, mbslen + 1) == (size_t) -1) { + goto err_free_wcs; + } + + for (wchar_t *wp = wcs; *wp != 0; wp++) { + wint_t wc = (wint_t)*wp; + switch(arg->i) { + case -1:{ + wc = towlower(wc); + }break; + case 0:{ + if (iswlower(wc)) + wc = towupper(wc); + else + wc = towlower(wc); + }break; + case 1:{ + wc = towupper(wc); + }break; + } + *wp = wc; + } + + size_t len = wcstombs(NULL, wcs, 0); + assert(len == text_range_size(sel)); + if (wcstombs(buf, wcs, text_range_size(sel) + 1) == (size_t) -1) { + goto err_free_wcs; + } + + if (!text_delete_range(txt, sel)) + continue; + if (!text_insert(vis, txt, sel.start, buf, len)) + continue; + free(buf); + } + + return keys; +err_free_wcs: + free(wcs); +err_free_buf: + free(buf); + return keys; +} + static KEY_ACTION_FN(ka_selections_clear) { View *view = vis_view(vis); From 80d62495ac62cac906c25620971e39df9f2d5dc1 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 6 Aug 2026 17:51:14 +0200 Subject: [PATCH 2/5] fix memory leak, skip selection on error and do not count the wcs length --- main.c | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/main.c b/main.c index a780cf052..2892e118b 100644 --- a/main.c +++ b/main.c @@ -365,22 +365,22 @@ static KEY_ACTION_FN(ka_selections_case) if (!text_range_valid(sel)) continue; - buf = text_bytes_alloc0(txt, sel.start, text_range_size(sel)); + size_t mblen = text_range_size(sel); + buf = text_bytes_alloc0(txt, sel.start, mblen); if (!buf) return keys; - size_t mbslen = mbstowcs(NULL, buf, 0); - if (mbslen == (size_t) -1) { - goto err_free_buf; - } - - wcs = calloc(mbslen + 1, sizeof(*wcs)); + // We know that the amount of wide-characters required to hold the + // selection is at MOST the amount of bytes in the selection. + wcs = calloc(mblen, sizeof(*wcs)); if (wcs == NULL) { - goto err_free_buf; + free(buf); + return keys; } - if (mbstowcs(wcs, buf, mbslen + 1) == (size_t) -1) { - goto err_free_wcs; + // This is safe as long as the multibyte string is 0-terminated. + if (mbstowcs(wcs, buf, mblen) == (size_t) -1) { + goto next_sel; } for (wchar_t *wp = wcs; *wp != 0; wp++) { @@ -402,24 +402,23 @@ static KEY_ACTION_FN(ka_selections_case) *wp = wc; } - size_t len = wcstombs(NULL, wcs, 0); - assert(len == text_range_size(sel)); - if (wcstombs(buf, wcs, text_range_size(sel) + 1) == (size_t) -1) { - goto err_free_wcs; + // We assume that the number of bytes required by the modified + // wide-character string is the same as the multibyte input. + assert(wcstombs(NULL, wcs, 0) == mblen); + if (wcstombs(buf, wcs, mblen) == (size_t) -1) { + goto next_sel; } if (!text_delete_range(txt, sel)) - continue; - if (!text_insert(vis, txt, sel.start, buf, len)) - continue; + goto next_sel; + if (!text_insert(vis, txt, sel.start, buf, mblen)) + goto next_sel; + +next_sel: + free(wcs); free(buf); } - return keys; -err_free_wcs: - free(wcs); -err_free_buf: - free(buf); return keys; } From 6ee7c5d97aa0f05110cf1295e5ccbb70fac43b60 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 6 Aug 2026 18:05:19 +0200 Subject: [PATCH 3/5] add description to selections case key action --- main.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.c b/main.c index 2892e118b..58bdd17c5 100644 --- a/main.c +++ b/main.c @@ -118,9 +118,9 @@ static Vis vis[1]; X(ka_selections_align, SELECTIONS_ALIGN, 0, "vis-selections-align", "Try to align all selections on the same column") \ X(ka_selections_align_indent, SELECTIONS_ALIGN_INDENT_LEFT, .i = -1, "vis-selections-align-indent-left", "Left-align all selections by inserting spaces") \ X(ka_selections_align_indent, SELECTIONS_ALIGN_INDENT_RIGHT, .i = +1, "vis-selections-align-indent-right", "Right-align all selections by inserting spaces") \ - X(ka_selections_case, SELECTIONS_CASE_TOUPPER, .i = +1, "vis-selections-case-toupper", "") \ - X(ka_selections_case, SELECTIONS_CASE_TOGGLE, .i = 0, "vis-selections-case-toggle", "") \ - X(ka_selections_case, SELECTIONS_CASE_TOLOWER, .i = -1, "vis-selections-case-tolower", "") \ + X(ka_selections_case, SELECTIONS_CASE_TOUPPER, .i = +1, "vis-selections-case-toupper", "Uppercase all selections") \ + X(ka_selections_case, SELECTIONS_CASE_TOGGLE, .i = 0, "vis-selections-case-toggle", "Toggle case of all selections") \ + X(ka_selections_case, SELECTIONS_CASE_TOLOWER, .i = -1, "vis-selections-case-tolower", "Lowercase all selections") \ X(ka_selections_clear, SELECTIONS_REMOVE_ALL, 0, "vis-selections-remove-all", "Remove all but the primary selection") \ X(ka_selections_complement, SELECTIONS_COMPLEMENT, 0, "vis-selections-complement", "Complement selections") \ X(ka_selections_intersect, SELECTIONS_INTERSECT, 0, "vis-selections-intersect", "Intersect with selections from mark") \ From 8c37c2f1b2d740637fd2b6882e0852eea7c482f2 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 6 Aug 2026 18:32:30 +0200 Subject: [PATCH 4/5] only use a single allocation for the wcs and the mbs Use malloc instead of calloc since we only care about a single 0-byte. Since we no longer 0-terminate the wcs the wcstombs(NULL, wcs, 0) call can no longer detect the amount of bytes required by the resulting multibyte string. --- main.c | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/main.c b/main.c index 58bdd17c5..5a99a528c 100644 --- a/main.c +++ b/main.c @@ -366,24 +366,27 @@ static KEY_ACTION_FN(ka_selections_case) continue; size_t mblen = text_range_size(sel); - buf = text_bytes_alloc0(txt, sel.start, mblen); - if (!buf) - return keys; - // We know that the amount of wide-characters required to hold the // selection is at MOST the amount of bytes in the selection. - wcs = calloc(mblen, sizeof(*wcs)); + + wcs = malloc(mblen * sizeof(*wcs) + mblen + 1); if (wcs == NULL) { - free(buf); return keys; } + buf = (char*)wcs + mblen * sizeof(*wcs); + buf[mblen] = 0; + + text_bytes_get(txt, sel.start, mblen, buf); + // This is safe as long as the multibyte string is 0-terminated. - if (mbstowcs(wcs, buf, mblen) == (size_t) -1) { + size_t wcslen = mbstowcs(wcs, buf, mblen); + if (wcslen == (size_t) -1) { goto next_sel; } - for (wchar_t *wp = wcs; *wp != 0; wp++) { + for (size_t i = 0; i < wcslen; i++) { + wchar_t *wp = wcs + i; wint_t wc = (wint_t)*wp; switch(arg->i) { case -1:{ @@ -404,7 +407,6 @@ static KEY_ACTION_FN(ka_selections_case) // We assume that the number of bytes required by the modified // wide-character string is the same as the multibyte input. - assert(wcstombs(NULL, wcs, 0) == mblen); if (wcstombs(buf, wcs, mblen) == (size_t) -1) { goto next_sel; } @@ -416,7 +418,6 @@ static KEY_ACTION_FN(ka_selections_case) next_sel: free(wcs); - free(buf); } return keys; From 613f36322a0c78c0131c8851343b981930a6e0a4 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Fri, 7 Aug 2026 08:37:06 +0200 Subject: [PATCH 5/5] keep the selection after case change and redraw to show changes --- main.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/main.c b/main.c index 5a99a528c..554912cff 100644 --- a/main.c +++ b/main.c @@ -416,10 +416,13 @@ static KEY_ACTION_FN(ka_selections_case) if (!text_insert(vis, txt, sel.start, buf, mblen)) goto next_sel; + view_selections_set(s, sel); + next_sel: free(wcs); } + vis_draw(vis); return keys; }