URI: 
       tAdd basic uppercase/lowercase support - ledit - Text editor (WIP)
  HTML git clone git://lumidify.org/ledit.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/ledit.git (encrypted, but very slow)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 4ee5ad27da617738b33a8e5e46aa75afcc2e54d0
   DIR parent a3b601c93fde71dd598f855980897a1e3ead492e
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Thu,  5 Oct 2023 12:10:03 +0200
       
       Add basic uppercase/lowercase support
       
       Diffstat:
         M Makefile                            |       8 ++++++--
         M README                              |       9 ++++++---
         M keys_basic.c                        |      99 ++++++++++++++++++++++++++++---
         M leditrc.5                           |      13 ++++++++++++-
         M leditrc.example                     |       2 ++
       
       5 files changed, 118 insertions(+), 13 deletions(-)
       ---
   DIR diff --git a/Makefile b/Makefile
       t@@ -13,6 +13,7 @@ MISCFILES = Makefile README LICENSE IDEAS NOTES TODO
        
        DEBUG=0
        SANITIZE=0
       +ENABLE_UTF8PROC=1
        
        OBJ = \
                assert.o \
       t@@ -70,12 +71,15 @@ EXTRA_CFLAGS_DEBUG0 = ${CFLAGS}
        EXTRA_LDFLAGS_DEBUG0 = ${LDFLAGS}
        EXTRA_CFLAGS_DEBUG1 = -DLEDIT_DEBUG -g
        EXTRA_FLAGS_SANITIZE1 = -fsanitize=address
       +EXTRA_CFLAGS_UTF8PROC0 = -DENABLE_UTF8PROC=0
       +EXTRA_CFLAGS_UTF8PROC1 = `pkg-config --cflags libutf8proc` -DENABLE_UTF8PROC=1
       +EXTRA_LDFLAGS_UTF8PROC1 = `pkg-config --libs libutf8proc`
        
        # Xcursor isn't actually needed right now since I'm not using the drag 'n drop functionality
        # of ctrlsel yet, but since it's moderately likely that I will use that in the future, I
        # decided to just leave it in.
       -CFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_CFLAGS_DEBUG${DEBUG}} -Wall -Wextra -pedantic -D_POSIX_C_SOURCE=200809L -std=c99 `pkg-config --cflags x11 xkbfile pangoxft xext xcursor`
       -LDFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_LDFLAGS_DEBUG${DEBUG}} `pkg-config --libs x11 xkbfile pangoxft xext xcursor` -lm
       +CFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_CFLAGS_DEBUG${DEBUG}} ${EXTRA_CFLAGS_UTF8PROC${ENABLE_UTF8PROC}} -Wall -Wextra -pedantic -D_POSIX_C_SOURCE=200809L -std=c99 `pkg-config --cflags x11 xkbfile pangoxft xext xcursor`
       +LDFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_LDFLAGS_DEBUG${DEBUG}} ${EXTRA_LDFLAGS_UTF8PROC${ENABLE_UTF8PROC}} `pkg-config --libs x11 xkbfile pangoxft xext xcursor` -lm
        
        all: ${BIN}
        
   DIR diff --git a/README b/README
       t@@ -8,15 +8,18 @@ layout.
        Additionally, it allows multiple views on a text buffer so that different
        parts of a file can be shown at the same time.
        
       -REQUIREMENTS: pango (with xft), xlib (extensions: xkb, xdbe), xcursor
       +REQUIREMENTS:
       +pango (with xft), xlib (extensions: xkb, xdbe),
       +xcursor, libutf8proc (if ENABLE_UTF8PROC is set)
        
        Note: xcursor technically wouldn't be needed since it's just a dependency
        of features in ctrlsel that currently aren't being used, but I will
        probably use those features later, so I was too lazy to remove those parts.
        
        Packages to install:
       -On OpenBSD: pango
       -On MX Linux: libpango1.0-dev, libx11-dev, libxkbfile-dev libxcursor-dev
       +On OpenBSD: pango, libutf8proc
       +On Debian-based systems:
       +libpango1.0-dev, libx11-dev, libxkbfile-dev libxcursor-dev, libutf8proc-dev
        (this is just from memory, I need to test it with a fresh system sometime)
        
        Installation:
   DIR diff --git a/keys_basic.c b/keys_basic.c
       t@@ -17,6 +17,7 @@
        /* FIXME: sort functions a bit better, maybe split file */
        /* FIXME: documentation */
        #include <stdio.h>
       +#include <ctype.h>
        #include <stdlib.h>
        
        #include <X11/Xlib.h>
       t@@ -27,6 +28,10 @@
        #include <X11/XF86keysym.h>
        #include <X11/cursorfont.h>
        
       +#if ENABLE_UTF8PROC
       +#include "utf8proc.h"
       +#endif
       +
        #include "util.h"
        #include "assert.h"
        #include "memory.h"
       t@@ -121,6 +126,8 @@ static struct action delete_graphemes_forwards_multiline(ledit_view *view, char 
        static struct action delete_graphemes_backwards_multiline(ledit_view *view, char *text, size_t len);
        static struct action yank(ledit_view *view, char *text, size_t len);
        static struct action yank_lines(ledit_view *view, char *text, size_t len);
       +static struct action uppercase(ledit_view *view, char *text, size_t len);
       +static struct action lowercase(ledit_view *view, char *text, size_t len);
        static struct action replace(ledit_view *view, char *text, size_t len);
        static struct action cursor_to_first_non_ws(ledit_view *view, char *text, size_t len);
        static struct action join_lines(ledit_view *view, char *text, size_t len);
       t@@ -160,6 +167,7 @@ static struct basic_key_cb basic_key_cb_map[] = {
                {"append-after-eol", &append_after_eol, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|VISUAL|INSERT},
                {"append-line-above", &append_line_above, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|VISUAL|INSERT},
                {"append-line-below", &append_line_below, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|VISUAL|INSERT},
       +        {"break-line", &break_line, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|INSERT},
                {"change", &change, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|VISUAL|INSERT},
                {"change-to-eol", &change_to_eol, KEY_FLAG_JUMP_TO_CURSOR, NORMAL},
                {"clipboard-copy", &clipcopy, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
       t@@ -185,17 +193,17 @@ static struct basic_key_cb basic_key_cb_map[] = {
                {"enter-searchedit-backwards", &enter_searchedit_backward, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT|VISUAL},
                {"enter-searchedit-forwards", &enter_searchedit_forward, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT|VISUAL},
                {"enter-visual", &enter_visual, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|INSERT},
       -        {"return-to-normal", &return_to_normal, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"find-char-backwards", &find_char_backwards, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"find-char-forwards", &find_char_forwards, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"find-next-char-backwards", &find_next_char_backwards, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"find-next-char-forwards", &find_next_char_forwards, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"insert-at-beginning", &insert_at_beginning, KEY_FLAG_JUMP_TO_CURSOR, NORMAL},
       +        {"insert-mark", &insert_mark, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"insert-text", &insert_mode_insert_text, KEY_FLAG_JUMP_TO_CURSOR, INSERT},
                {"join-lines", &join_lines, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|INSERT},
                {"jump-to-mark", &jump_to_mark, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"key-0", &key_0, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
       -        {"insert-mark", &insert_mark, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
       +        {"lowercase", &lowercase, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|INSERT},
                {"move-to-eol", &move_to_eol, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"move-to-line", &move_to_line, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"next-bigword", &next_bigword, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
       t@@ -219,7 +227,7 @@ static struct basic_key_cb basic_key_cb_map[] = {
                {"redo", &redo, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|INSERT},
                {"repeat-command", &repeat_command, KEY_FLAG_JUMP_TO_CURSOR, NORMAL},
                {"replace", &replace, KEY_FLAG_JUMP_TO_CURSOR, NORMAL},
       -        {"break-line", &break_line, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|INSERT},
       +        {"return-to-normal", &return_to_normal, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"screen-down", &screen_down, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
                {"screen-up", &screen_up, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
                {"scroll-lines-down", &scroll_lines_down, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
       t@@ -232,6 +240,7 @@ static struct basic_key_cb basic_key_cb_map[] = {
                {"switch-selection-end", &switch_selection_end, KEY_FLAG_JUMP_TO_CURSOR, VISUAL},
                {"toggle-hard-line-based", &toggle_hard_line_based, KEY_FLAG_NONE|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"undo", &undo, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|INSERT},
       +        {"uppercase", &uppercase, KEY_FLAG_JUMP_TO_CURSOR, NORMAL|INSERT},
                {"yank", &yank, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|VISUAL|INSERT},
                {"yank-lines", &yank_lines, KEY_FLAG_JUMP_TO_CURSOR|KEY_FLAG_LOCK_ALLOWED, NORMAL|INSERT},
        };
       t@@ -2495,6 +2504,80 @@ GEN_MOVE_TO_CHAR(
        )
        
        static struct action
       +loweruppercase(ledit_view *view, int upper) {
       +        /* FIXME: shouldn't CHECK_VIEW_LOCKED be in a lot more places? */
       +        CHECK_VIEW_LOCKED(view);
       +        /* FIXME: move most of this to convenience functions in buffer.c */
       +        ledit_line *line = buffer_get_line(view->buffer, view->cur_line);
       +        if (view->cur_index >= line->len)
       +                return (struct action){ACTION_NONE, NULL};
       +        buffer_normalize_line(line);
       +        size_t start_index = view->cur_index;
       +#if ENABLE_UTF8PROC
       +        utf8proc_int32_t c;
       +        /* FIXME: this cast to utf8proc_uint8_t probably doesn't break anything, but could it? */
       +        utf8proc_ssize_t origlen = utf8proc_iterate((utf8proc_uint8_t *)line->text + start_index, line->len - start_index, &c);
       +        /* FIXME: show error message? */
       +        if (c < 0 || origlen < 1)
       +                return (struct action){ACTION_NONE, NULL};
       +        utf8proc_int32_t u;
       +        if (upper)
       +                u = utf8proc_toupper(c);
       +        else
       +                u = utf8proc_tolower(c);
       +        utf8proc_uint8_t u8[4];
       +        utf8proc_ssize_t newlen = utf8proc_encode_char(u, u8);
       +        if (newlen < 1)
       +                return (struct action){ACTION_NONE, NULL};
       +        delete_range(
       +            view, 0, 0,
       +            view->cur_line, start_index, view->cur_line, start_index + origlen, 0
       +        );
       +        insert_text(
       +            view, view->cur_line, start_index, (char *)u8, newlen,
       +            view->cur_line, start_index, view->cur_line, start_index, 1, 1, 0
       +        );
       +#else
       +        char c;
       +        if (upper)
       +                c = toupper((unsigned char)line->text[view->cur_index]);
       +        else
       +                c = tolower((unsigned char)line->text[view->cur_index]);
       +        delete_range(
       +            view, 0, 0,
       +            view->cur_line, start_index, view->cur_line, start_index + 1, 0
       +        );
       +        insert_text(
       +            view, view->cur_line, start_index, &c, 1,
       +            view->cur_line, start_index, view->cur_line, start_index, 1, 1, 0
       +        );
       +#endif
       +        /* If the last character on a line is replaced, the cursor would jump
       +           backwards due to the deletion, so it has to be set to the original
       +           position again */
       +        view->cur_index = start_index;
       +        push_undo_empty_insert(view, view->cur_line, view->cur_index, 0);
       +        if (view->mode == NORMAL)
       +                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
       +        finalize_repetition_stack();
       +        return (struct action){ACTION_NONE, NULL};
       +}
       +
       +static struct action
       +uppercase(ledit_view *view, char *text, size_t len) {
       +        (void)text;
       +        (void)len;
       +        return loweruppercase(view, 1);
       +}
       +
       +static struct action
       +lowercase(ledit_view *view, char *text, size_t len) {
       +        (void)text;
       +        (void)len;
       +        return loweruppercase(view, 0);
       +}
       +
       +static struct action
        replace_cb(ledit_view *view, char *text, size_t len) {
                CHECK_VIEW_LOCKED(view);
                size_t start_index = view->cur_index;
       t@@ -2512,11 +2595,13 @@ replace_cb(ledit_view *view, char *text, size_t len) {
                    view, view->cur_line, start_index, text, len,
                    view->cur_line, start_index, view->cur_line, start_index, 1, 1, 0
                );
       -        /* this should not be necessary, but just in case */
       -        view->cur_index = view_get_legal_normal_pos(
       -            view, view->cur_line, view->cur_index
       -        );
       +        /* If the last character on a line is replaced, the cursor would jump
       +           backwards due to the deletion, so it has to be set to the original
       +           position again */
       +        view->cur_index = start_index;
                push_undo_empty_insert(view, view->cur_line, view->cur_index, 0);
       +        if (view->mode == NORMAL)
       +                view_set_line_cursor_attrs(view, view->cur_line, view->cur_index);
                grab_char_cb = NULL;
                finalize_repetition_stack();
                return (struct action){ACTION_NONE, NULL};
   DIR diff --git a/leditrc.5 b/leditrc.5
       t@@ -1,4 +1,4 @@
       -.Dd October 2, 2023
       +.Dd October 5, 2023
        .Dt LEDITRC 5
        .Os
        .Sh NAME
       t@@ -674,6 +674,17 @@ write, and current line number.
        Switch the end of the selection that can be moved.
        .It Ar toggle-hard-line-based Op normal, visual, insert
        Toggle the line mode between hardline and softline.
       +.It Ar uppercase Op normal, insert
       +.It Ar lowercase Op normal, insert
       +Replace the character at the current cursor position with the uppercase/lowercase version if it exists.
       +If utf8proc support is not enabled, this will use the standard C library functions
       +.Fn toupper
       +and
       +.Fn tolower ,
       +so it will not work with most Unicode characters.
       +Note that even with utf8proc, it will not work in all cases because some characters require
       +more complex handling (e.g. characters that require multiple characters when converted to
       +uppercase), which is not supported.
        .It Ar undo Oo normal, insert Oc Oo num Oc
        Undo
        .Ar num
   DIR diff --git a/leditrc.example b/leditrc.example
       t@@ -107,6 +107,8 @@ bindings = {
                        bind find-next-char-backwards text "T" modes normal|visual
                        bind find-char-forwards text "f" modes normal|visual
                        bind find-char-backwards text "F" modes normal|visual
       +                bind uppercase text "U"  modes normal|insert mods control
       +                bind lowercase text "L"  modes normal|insert mods control
                        bind insert-text catchall modes insert
                }
                command-keys = {