From fb553ba1c233466428a8acf54f917f3e11d5910b Mon Sep 17 00:00:00 2001 From: Mike Ashton Date: Tue, 10 Feb 2026 14:55:14 +0000 Subject: [PATCH 1/3] Multiple changes (sorry), mostly driven by AI. These correct some issues I was having with this version of tf not talking utf8 correctly to my utf8 enabled talker, plus it addresses some of the byte-versus-character spacing issues in input processing mentioned in the change logs. It does change the default character to utf8, which I appreciate is an invasive change. I'm supporting that because, at this point in history, it feels more indicated than precipitous. --- Makefile | 26 +++++------- src/expr.c | 8 ++++ src/funclist.h | 2 + src/keyboard.c | 107 ++++++++++++++++++++++++++++++++++++++++++----- src/util.c | 81 +++++++++++++++++++++++++++++++++++ src/util.h | 11 ++++- src/varlist.h | 2 +- tf-lib/kbfunc.tf | 4 +- 8 files changed, 209 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index 888eff1..880bd99 100644 --- a/Makefile +++ b/Makefile @@ -8,22 +8,16 @@ # ######################################################################## -# Note: the space on the end of the next line is intentional, so it will -# still work in unix for idiots who got ^M on the end of every line. -default: all - -all: - @echo : - @echo : To build TinyFugue, use one of the following commands - @echo : appropriate to your system. For more information, see - @echo : the README file in the directory for your system. - @echo : - @echo : UNIX, CYGWIN, MACOS X: - @echo : ./configure [options] - @echo : make install - @echo : - -install files tf clean uninstall: all + + +LONGPATH="${PATH}:@PATH@" + +default: files + +files all install tf clean uninstall: _force_ + @cd src; PATH="${LONGPATH}" "${MAKE}" $@ + +_force_: # The next line is a hack to get around a bug in BSD/386 make. make: diff --git a/src/expr.c b/src/expr.c index 72473b5..aea57d6 100644 --- a/src/expr.c +++ b/src/expr.c @@ -1415,6 +1415,14 @@ static Value *function_switch(const ExprFunc *func, int n, const char *parent) case FN_kbpoint: return newint(keyboard_pos); + case FN_kbpoint_prev: + return newint(utf8_prev_n_chars(keybuf->data, keybuf->len, + keyboard_pos, n > 0 ? opdint(1) : 1)); + + case FN_kbpoint_next: + return newint(utf8_next_n_chars(keybuf->data, keybuf->len, + keyboard_pos, n > 0 ? opdint(1) : 1)); + case FN_kbgoto: return newint(igoto(opdint(1))); diff --git a/src/funclist.h b/src/funclist.h index 47901e7..f7b6b02 100644 --- a/src/funclist.h +++ b/src/funclist.h @@ -51,6 +51,8 @@ funccode(kbhead, 0, 0, 0), funccode(kblen, 0, 0, 0), funccode(kbmatch, 0, 0, 1), funccode(kbpoint, 0, 0, 0), +funccode(kbpoint_next, 0, 0, 1), +funccode(kbpoint_prev, 0, 0, 1), funccode(kbtail, 0, 0, 0), funccode(kbwordleft, 0, 0, 1), funccode(kbwordright, 0, 0, 1), diff --git a/src/keyboard.c b/src/keyboard.c index f72ca5d..0267231 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -26,6 +26,10 @@ #include "cmdlist.h" #include "variable.h" /* unsetvar() */ +#if WIDECHAR +#include +#endif + static int literal_next = FALSE; static TrieNode *keynode = NULL; /* current node matched by input */ static int kbnum_internal = 0; @@ -140,16 +144,20 @@ int handle_keyboard_input(int read_flag) for (i = 0; i < count; i++) { #if !WIDECHAR - if (istrip) buf[i] &= 0x7F; - if (buf[i] & 0x80) { - if (!literal_next && - (meta_esc == META_ON || (!is_print(buf[i]) && meta_esc))) - { - Stringadd(current_input, '\033'); - buf[i] &= 0x7F; - } - if (!is_print(buf[i])) - buf[i] &= 0x7F; + /* Only apply 7-bit stripping to bytes 0x00-0x7F so UTF-8 (0x80-0xFF) + * is preserved. */ + if ((unsigned char)buf[i] < 0x80) { + if (istrip) buf[i] &= 0x7F; + if (buf[i] & 0x80) { + if (!literal_next && + (meta_esc == META_ON || (!is_print(buf[i]) && meta_esc))) + { + Stringadd(current_input, '\033'); + buf[i] &= 0x7F; + } + if (!is_print(buf[i])) + buf[i] &= 0x7F; + } } #endif Stringadd(current_input, mapchar(buf[i])); @@ -180,7 +188,26 @@ int handle_keyboard_input(int read_flag) } else if (s[key_start] == '\b' || s[key_start] == '\177') { handle_input_string(s + input_start, key_start - input_start); place = input_start = ++key_start; +#if WIDECHAR + /* + * In wide-character builds, treat backspace/delete as + * operating on whole UTF-8 characters rather than raw bytes, + * so we never leave the input buffer containing a partial + * multibyte sequence. + */ + { + int del_from = keyboard_pos; + int count = kbnumval; + + if (count < 0) + count = -count; + del_from = utf8_prev_n_chars(keybuf->data, keybuf->len, + del_from, count); + do_kbdel(del_from); + } +#else do_kbdel(keyboard_pos - kbnumval); +#endif reset_kbnum(); } else if (kbnum && is_digit(s[key_start]) && key_start == input_start) @@ -388,15 +415,56 @@ int do_kbdel(int place) #define is_inword(c) (is_alnum(c) || (wordpunct && strchr(wordpunct, (c)))) +#if WIDECHAR +/* Return true if the UTF-8 character at byte offset pos in s[0..len-1] is a + * word character (alphanumeric or in wordpunct). pos must be at the start + * of a character. */ +static int is_word_char_at(const char *s, int len, int pos) +{ + wchar_t wc; + mbstate_t mbs; + size_t n; + + if (!s || pos < 0 || pos >= len) + return 0; + memset(&mbs, 0, sizeof(mbs)); + n = mbrtowc(&wc, s + pos, (size_t)(len - pos), &mbs); + if (n == (size_t)-1 || n == (size_t)-2 || n == 0) + return 0; + if (wc >= 0 && wc < 128 && wordpunct != NULL && strchr(wordpunct, (char)wc)) + return 1; + return iswalnum((wint_t)wc); +} +#endif + int do_kbword(int start, int dir) { int stop = (dir < 0) ? -1 : keybuf->len; int place = start<0 ? 0 : start>keybuf->len ? keybuf->len : start; place -= (dir < 0); +#if WIDECHAR + /* Normalize to start of character when in the middle of a multibyte. */ + if (place >= 0 && place < keybuf->len && + ((unsigned char)keybuf->data[place] & 0xC0) == 0x80) + place = utf8_prev_char(keybuf->data, keybuf->len, place); + + /* Skip non-word characters; stop at buffer boundary. */ + while ((dir >= 0 ? place != stop : place > 0) && + !is_word_char_at(keybuf->data, keybuf->len, place)) + place = (dir < 0) ? utf8_prev_char(keybuf->data, keybuf->len, place) + : utf8_next_char(keybuf->data, keybuf->len, place); + /* Skip word characters. */ + while ((dir >= 0 ? place != stop : place > 0) && + is_word_char_at(keybuf->data, keybuf->len, place)) + place = (dir < 0) ? utf8_prev_char(keybuf->data, keybuf->len, place) + : utf8_next_char(keybuf->data, keybuf->len, place); + return place; +#else while (place != stop && !is_inword(keybuf->data[place])) place += dir; while (place != stop && is_inword(keybuf->data[place])) place += dir; return place + (dir < 0); +#endif } int do_kbmatch(int start) @@ -406,10 +474,20 @@ int do_kbmatch(int start) int dir, stop, depth = 0; int place = start<0 ? 0 : start>keybuf->len ? keybuf->len : start; +#if WIDECHAR + /* Ensure we start at a character boundary. */ + if (place < keybuf->len && ((unsigned char)keybuf->data[place] & 0xC0) == 0x80) + place = utf8_prev_char(keybuf->data, keybuf->len, place); +#endif + while (1) { if (place >= keybuf->len) return -1; if ((type = strchr(braces, keybuf->data[place]))) break; +#if WIDECHAR + place = utf8_next_char(keybuf->data, keybuf->len, place); +#else ++place; +#endif } dir = ((type - braces) % 2) ? -1 : 1; stop = (dir < 0) ? -1 : keybuf->len; @@ -417,7 +495,14 @@ int do_kbmatch(int start) if (keybuf->data[place] == type[0]) depth++; else if (keybuf->data[place] == type[dir]) depth--; if (depth == 0) return place; - } while ((place += dir) != stop); +#if WIDECHAR + place = (dir < 0) ? utf8_prev_char(keybuf->data, keybuf->len, place) + : utf8_next_char(keybuf->data, keybuf->len, place); + if (dir < 0 && place <= 0) break; +#else + place += dir; +#endif + } while (place != stop); return -1; } diff --git a/src/util.c b/src/util.c index fc5ba83..95a8826 100644 --- a/src/util.c +++ b/src/util.c @@ -189,6 +189,87 @@ const conString *print_to_ascii(String *dest, const char *src) return CS(dest); } +/* + * UTF-8 helper functions + * + * These helpers operate on byte indices into UTF-8 strings and ensure that + * editing operations do not split multibyte sequences. They make no attempt + * to validate that the underlying data is well‑formed UTF-8; they simply + * respect continuation byte patterns. + */ + +int utf8_prev_char(const char *s, int len, int pos) +{ + int p; + + if (!s || len <= 0 || pos <= 0) + return 0; + if (pos > len) + pos = len; + + p = pos - 1; + /* Walk backwards over UTF-8 continuation bytes (10xxxxxx). */ + while (p > 0 && ((unsigned char)s[p] & 0xC0) == 0x80) + --p; + return p; +} + +int utf8_next_char(const char *s, int len, int pos) +{ + int p; + + if (!s || len <= 0) + return 0; + if (pos < 0) + pos = 0; + if (pos >= len) + return len; + + p = pos + 1; + /* Skip over any continuation bytes to reach the next character start. */ + while (p < len && ((unsigned char)s[p] & 0xC0) == 0x80) + ++p; + return p; +} + +int utf8_prev_n_chars(const char *s, int len, int pos, int n) +{ + int p = pos; + + if (!s || len <= 0) + return 0; + if (p < 0) + p = 0; + if (p > len) + p = len; + if (n <= 0) + return p; + + while (n-- > 0 && p > 0) { + p = utf8_prev_char(s, len, p); + } + return p; +} + +int utf8_next_n_chars(const char *s, int len, int pos, int n) +{ + int p = pos; + + if (!s || len <= 0) + return len; + if (p < 0) + p = 0; + if (p > len) + p = len; + if (n <= 0) + return p; + + while (n-- > 0 && p < len) { + p = utf8_next_char(s, len, p); + } + return p; +} + /* String handlers * These are heavily used functions, so speed is favored over simplicity. */ diff --git a/src/util.h b/src/util.h index b63c808..0f2936a 100644 --- a/src/util.h +++ b/src/util.h @@ -28,8 +28,9 @@ struct feature { #define mapchar(c) ((c) ? (c) & 0xFF : 0x80) #define unmapchar(c) ((char)(((c) == (char)0x80) ? 0x0 : (c))) -/* Map character into set allowed by locale */ -#define localize(c) ((is_print(c) || is_cntrl(c)) ? (c) : (c) & 0x7F) +/* Map character into set allowed by locale. Preserve bytes 0x80-0xFF so + * UTF-8 and other 8-bit encodings are not stripped to ASCII. */ +#define localize(c) ((is_print(c) || is_cntrl(c) || (unsigned char)(c) >= 0x80) ? (c) : (c) & 0x7F) #endif /* Note STRNDUP works only if src[len] == '\0', ie. len == strlen(src) */ @@ -119,6 +120,12 @@ extern int ch_mailfile(Var *var); extern int ch_maildelay(Var *var); extern void check_mail(void); +/* UTF-8 helpers for safe byte indexing */ +extern int utf8_prev_char(const char *s, int len, int pos); +extern int utf8_next_char(const char *s, int len, int pos); +extern int utf8_prev_n_chars(const char *s, int len, int pos, int n); +extern int utf8_next_n_chars(const char *s, int len, int pos, int n); + extern type_t string_arithmetic_type(const char *str, int typeset); extern Value *parsenumber(const char *str, const char **caller_endp, int typeset, Value *val); diff --git a/src/varlist.h b/src/varlist.h index b092a2b..1aa9f14 100644 --- a/src/varlist.h +++ b/src/varlist.h @@ -67,7 +67,7 @@ varflag(VAR_borg, "borg", TRUE, NULL) varenum(VAR_cecho, "cecho", 0, NULL, enum_mecho) varstr (VAR_cecho_attr, "cecho_attr", "Cgreen", ch_attr) #if WIDECHAR -varstr (VAR_default_charset,"default_charset","US-ASCII",ch_default_charset) +varstr (VAR_default_charset,"default_charset","UTF-8",ch_default_charset) #endif varflag(VAR_cleardone, "cleardone", FALSE, NULL) varflag(VAR_clearfull, "clearfull", FALSE, NULL) diff --git a/tf-lib/kbfunc.tf b/tf-lib/kbfunc.tf index eceba2c..323a959 100644 --- a/tf-lib/kbfunc.tf +++ b/tf-lib/kbfunc.tf @@ -35,7 +35,7 @@ /def -i dokey_dword = /kb_kill_word /def -i dokey_end = /@test kbgoto(kblen()) /def -i dokey_home = /@test kbgoto(0) -/def -i dokey_left = /@test kbgoto(kbpoint() - (kbnum?:1)) +/def -i dokey_left = /@test kbgoto(kbpoint_prev(kbnum?:1)) /def -i dokey_lnext = /dokey lnext /def -i dokey_newline = /dokey newline /def -i dokey_pause = /dokey pause @@ -44,7 +44,7 @@ /def -i dokey_recallend = /dokey recallend /def -i dokey_recallf = /dokey recallf /def -i dokey_redraw = /dokey redraw -/def -i dokey_right = /@test kbgoto(kbpoint() + (kbnum?:1)) +/def -i dokey_right = /@test kbgoto(kbpoint_next(kbnum?:1)) /def -i dokey_searchb = /dokey searchb /def -i dokey_searchf = /dokey searchf /def -i dokey_socketb = /fg -c$[-kbnum?:-1] From f771cbab9e95e5f282be64b8a6254142bdf39c3f Mon Sep 17 00:00:00 2001 From: Mike Ashton Date: Tue, 10 Feb 2026 15:31:30 +0000 Subject: [PATCH 2/3] There were some problems with line editing - dodgy byte/characters maths mixed with transpose being a tf function rather than a C function. The results of the limited testing I've done seems pretty nice - transposing of superscripts all works like it should, and edits happen in the right place, look right and have the expected outcome --- src/default | 2 +- src/expr.c | 3 + src/funclist.h | 1 + src/keyboard.c | 76 +++++++++++++ src/keyboard.h | 1 + src/output.c | 279 ++++++++++++++++++++++++++++++++++++++++++++--- src/util.c | 49 +++++++++ src/util.h | 4 + tf-lib/kbfunc.tf | 9 +- 9 files changed, 398 insertions(+), 26 deletions(-) diff --git a/src/default b/src/default index b49c54e..72fdc3b 120000 --- a/src/default +++ b/src/default @@ -1 +1 @@ -/usr/local/bin/tf \ No newline at end of file +/home/mike/bin/tf \ No newline at end of file diff --git a/src/expr.c b/src/expr.c index aea57d6..48b16ba 100644 --- a/src/expr.c +++ b/src/expr.c @@ -1429,6 +1429,9 @@ static Value *function_switch(const ExprFunc *func, int n, const char *parent) case FN_kbdel: return (newint(do_kbdel(opdint(1)))); + case FN_kbtranspose: + return newint(do_kbtranspose()); + case FN_kbmatch: return newint(do_kbmatch(n>0 ? opdint(1) : keyboard_pos)); diff --git a/src/funclist.h b/src/funclist.h index f7b6b02..7dad80f 100644 --- a/src/funclist.h +++ b/src/funclist.h @@ -54,6 +54,7 @@ funccode(kbpoint, 0, 0, 0), funccode(kbpoint_next, 0, 0, 1), funccode(kbpoint_prev, 0, 0, 1), funccode(kbtail, 0, 0, 0), +funccode(kbtranspose, 0, 0, 0), funccode(kbwordleft, 0, 0, 1), funccode(kbwordright, 0, 0, 1), funccode(keycode, 0, 1, 1), diff --git a/src/keyboard.c b/src/keyboard.c index 0267231..16a27b2 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -413,6 +413,82 @@ int do_kbdel(int place) return keyboard_pos; } +/* Transpose the character before the cursor with the character at (or before) the + * cursor. At end of line, swaps the last two characters. Returns new keyboard_pos + * or 0 on failure (beep). UTF-8 aware when WIDECHAR. */ +int do_kbtranspose(void) +{ + int left_start, left_end, right_start, right_end; + char left_buf[8], right_buf[8]; /* UTF-8 char can be up to 4 bytes */ + int left_len, right_len, tail_len; + + if (!keybuf->len) { + dobell(1); + return 0; + } + if (keyboard_pos == 0) { + dobell(1); + return 0; + } + +#if WIDECHAR + left_end = keyboard_pos; + left_start = utf8_prev_char(keybuf->data, keybuf->len, left_end); + if (keyboard_pos < keybuf->len) { + right_start = keyboard_pos; + right_end = utf8_next_char(keybuf->data, keybuf->len, right_start); + } else { + /* At end of line: swap last two characters. */ + right_start = left_start; + right_end = left_end; + left_start = utf8_prev_char(keybuf->data, keybuf->len, left_start); + left_end = right_start; + } +#else + /* Single-byte: swap the byte before cursor with the byte at cursor (or last byte). */ + left_start = keyboard_pos - 1; + left_end = keyboard_pos; + if (keyboard_pos < keybuf->len) { + right_start = keyboard_pos; + right_end = keyboard_pos + 1; + } else { + right_start = left_start; + right_end = left_end; + left_start = keyboard_pos - 2; + left_end = keyboard_pos - 1; + if (left_start < 0) { + dobell(1); + return 0; + } + } +#endif + + left_len = left_end - left_start; + right_len = right_end - right_start; + if (left_len > (int)sizeof(left_buf) || right_len > (int)sizeof(right_buf)) { + dobell(1); + return 0; + } + + memcpy(left_buf, keybuf->data + left_start, left_len); + memcpy(right_buf, keybuf->data + right_start, right_len); + tail_len = keybuf->len - right_end; + if (tail_len > 0) + Stringncpy(scratch, keybuf->data + right_end, tail_len); + + Stringtrunc(keybuf, left_start); + Stringncat(keybuf, right_buf, right_len); + Stringncat(keybuf, left_buf, left_len); + if (tail_len > 0) + SStringcat(keybuf, CS(scratch)); + + keyboard_pos = left_start + right_len; + sync_input_hist(); + set_refresh_pending(REF_LOGICAL); + do_refresh(); + return keyboard_pos; +} + #define is_inword(c) (is_alnum(c) || (wordpunct && strchr(wordpunct, (c)))) #if WIDECHAR diff --git a/src/keyboard.h b/src/keyboard.h index b294437..e64791e 100644 --- a/src/keyboard.h +++ b/src/keyboard.h @@ -19,6 +19,7 @@ extern int bind_key(Macro *spec, const char *key); extern void unbind_key(const char *key); extern struct Macro *find_key(const char *key); extern int do_kbdel(int place); +extern int do_kbtranspose(void); extern int do_kbword(int start, int dir); extern int do_kbmatch(int start); extern int handle_keyboard_input(int read_flag); diff --git a/src/output.c b/src/output.c index 04896f4..d82ec20 100644 --- a/src/output.c +++ b/src/output.c @@ -2209,25 +2209,35 @@ static void ictrl_put(const char *s, int n) /* ioutputs * Print string within bounds of input window. len is the number of - * characters to print; return value is the number actually printed, - * which may be less than len if string doesn't fit in the input window. + * bytes to print; return value is the number of bytes actually printed. * precondition: iendx,iendy and real cursor are at the output position. + * When WIDECHAR, iendx and cx advance by column (character) count, not byte count. */ static int ioutputs(const char *str, int len) { - int space, written; + int space, written, n, cols; - for (written = 0; len > 0; len -= space) { - if ((space = Wrap - iendx + 1) <= 0) { + for (written = 0; len > 0; ) { + space = Wrap - iendx + 1; /* column budget */ + if (space <= 0) { if (!visual || iendy == lines) break; /* at end of window */ if (visual) xy(iendx = 1, ++iendy); space = Wrap; } +#if WIDECHAR + n = utf8_bytes_for_columns(str, len, space); + if (n <= 0) break; + cols = utf8_char_count(str, n); +#else if (space > len) space = len; - ictrl_put(str, space); cx += space; - str += space; - written += space; - iendx += space; + n = space; + cols = n; +#endif + ictrl_put(str, n); cx += cols; + str += n; + written += n; + len -= n; + iendx += cols; } return written; } @@ -2256,19 +2266,67 @@ void iput(int len) { const char *s; int count, scrolled = 0, oiex = iendx, oiey = iendy; +#if WIDECHAR + int col_adv, col_space; +#endif s = keybuf->data + keyboard_pos - len; if (!sockecho()) return; if (visual) physical_refresh(); +#if WIDECHAR + col_adv = utf8_char_count(s, len); + col_space = Wrap - (cx - 1); +#endif if (keybuf->len - keyboard_pos > 8 && /* faster than redisplay? */ visual && insert && clear_to_eol && (insert_char || insert_start) && /* can insert */ +#if WIDECHAR + cy + (cx - 1 + col_adv) / Wrap <= lines && + Wrap - col_adv > 8) +#else cy + (cx + len) / Wrap <= lines && /* new text will fit in window */ Wrap - len > 8) /* faster than redisplay? */ +#endif { /* fast insert */ +#if WIDECHAR + iy = iy + (ix - 1 + col_adv) / Wrap; + ix = (ix - 1 + col_adv) % Wrap + 1; + iendy = iendy + (iendx - 1 + col_adv) / Wrap; + iendx = (iendx - 1 + col_adv) % Wrap + 1; + if (iendy > lines) { iendy = lines; iendx = Wrap + 1; } + if (cx - 1 + col_adv <= Wrap) { + if (insert_start) tp(insert_start); + else for (count = col_adv; count; count--) tp(insert_char); + ictrl_put(s, len); + s += utf8_bytes_for_columns(s, len, col_space); + cx += col_adv; + } else { + ictrl_put(s, utf8_bytes_for_columns(s, len, col_space)); + s += utf8_bytes_for_columns(s, len, col_space); + cx = Wrap + 1; + if (insert_start) tp(insert_start); + } + while (s < keybuf->data + keybuf->len) { + if (Wrap < columns && cx <= Wrap) { + xy(Wrap + 1, cy); + tp(clear_to_eol); + } + if (cy == lines) break; + xy(1, cy + 1); + if (!insert_start) + for (count = col_adv; count; count--) tp(insert_char); + { + int chunk = (int)(keybuf->data + keybuf->len - s); + int n = utf8_bytes_for_columns(s, chunk, Wrap); + ictrl_put(s, n); + cx += utf8_char_count(s, n); + s += n; + } + } +#else iy = iy + (ix - 1 + len) / Wrap; ix = (ix - 1 + len) % Wrap + 1; iendy = iendy + (iendx - 1 + len) / Wrap; @@ -2298,6 +2356,7 @@ void iput(int len) ictrl_put(s, len); cx += len; s += Wrap; } +#endif if (insert_start) tp(insert_end); ipos(); bufflush(); @@ -2392,11 +2451,20 @@ void idel(int place) { int len; int oiey = iendy; +#if WIDECHAR + int col_diff, start_byte; +#endif if ((len = place - keyboard_pos) < 0) keyboard_pos = place; if (!sockecho()) return; +#if WIDECHAR + start_byte = (len < 0) ? place : keyboard_pos; + col_diff = utf8_char_count(keybuf->data + start_byte, (len < 0) ? -len : len); + if (len < 0) ix -= col_diff; +#else if (len < 0) ix += len; - +#endif + if (!visual) { int prompt_len = prompt ? prompt->len % Wrap : 0; if (ix < prompt_len + 1 || need_refresh) { @@ -2408,7 +2476,11 @@ void idel(int place) physical_refresh(); return; } +#if WIDECHAR + if (len < 0) { bufputnc('\010', col_diff); cx -= col_diff; } +#else if (len < 0) { bufputnc('\010', -len); cx += len; } +#endif } else { /* visual */ @@ -2424,14 +2496,30 @@ void idel(int place) } if (len < 0) len = -len; +#if WIDECHAR + /* col_diff already set; use for display operations */ +#else + (void)0; +#endif if (visual && delete_char && keybuf->len - keyboard_pos > 3 && len < Wrap/3) { /* hardware method */ - int i, space, pos; + int i, space, pos, n; iendy = iy; +#if WIDECHAR + if (ix + col_diff <= Wrap) { + for (i = col_diff; i; i--) tp(delete_char); + iendx = Wrap + 1 - col_diff; + } else { + iendx = ix; + } + /* First byte to display is after the deleted col_diff characters */ + pos = utf8_column_to_byte(keybuf->data, keybuf->len, + utf8_byte_to_column(keybuf->data, keybuf->len, keyboard_pos) + col_diff); +#else if (ix + len <= Wrap) { for (i = len; i; i--) tp(delete_char); iendx = Wrap + 1 - len; @@ -2439,21 +2527,37 @@ void idel(int place) iendx = ix; } pos = keyboard_pos - ix + iendx; +#endif while (pos < keybuf->len) { if ((space = Wrap - iendx + 1) <= 0) { if (iendy == lines) break; /* at end of window */ xy(iendx = 1, ++iendy); +#if WIDECHAR + for (i = col_diff; i; i--) tp(delete_char); +#else for (i = len; i; i--) tp(delete_char); space = Wrap - len; - if (space > keybuf->len - pos) space = keybuf->len - pos; +#endif } else { xy(iendx, iendy); - if (space > keybuf->len - pos) space = keybuf->len - pos; - ictrl_put(keybuf->data + pos, space); cx += space; +#if WIDECHAR + n = utf8_bytes_for_columns(keybuf->data + pos, keybuf->len - pos, space); +#else + n = (space > keybuf->len - pos) ? keybuf->len - pos : space; +#endif + if (n > keybuf->len - pos) n = keybuf->len - pos; + ictrl_put(keybuf->data + pos, n); +#if WIDECHAR + cx += utf8_char_count(keybuf->data + pos, n); + iendx += utf8_char_count(keybuf->data + pos, n); + pos += n; +#else + cx += n; + iendx += n; + pos += n; +#endif } - iendx += space; - pos += space; } /* erase tail */ @@ -2469,6 +2573,18 @@ void idel(int place) ioutputs(keybuf->data + keyboard_pos, keybuf->len - keyboard_pos); /* erase tail */ +#if WIDECHAR + if (col_diff > Wrap - cx + 1) col_diff = Wrap - cx + 1; + if (visual && clear_to_eos && (col_diff > 2 || cy < oiey)) { + tp(clear_to_eos); + } else if (clear_to_eol && col_diff > 2) { + tp(clear_to_eol); + if (visual && cy < oiey) clear_lines(cy + 1, oiey); + } else { + bufputnc(' ', col_diff); cx += col_diff; + if (visual && cy < oiey) clear_lines(cy + 1, oiey); + } +#else if (len > Wrap - cx + 1) len = Wrap - cx + 1; if (visual && clear_to_eos && (len > 2 || cy < oiey)) { tp(clear_to_eos); @@ -2479,6 +2595,7 @@ void idel(int place) bufputnc(' ', len); cx += len; if (visual && cy < oiey) clear_lines(cy + 1, oiey); } +#endif } /* restore cursor */ @@ -2491,12 +2608,22 @@ void idel(int place) int igoto(int place) { int diff; +#if WIDECHAR + int diff_col; + int old_col, new_col; +#endif if (place < 0) place = 0; if (place > keybuf->len) place = keybuf->len; diff = place - keyboard_pos; +#if WIDECHAR + /* Compute movement in character cells, not bytes. */ + old_col = utf8_byte_to_column(keybuf->data, keybuf->len, keyboard_pos); + new_col = utf8_byte_to_column(keybuf->data, keybuf->len, place); + diff_col = new_col - old_col; +#endif keyboard_pos = place; if (!diff) { @@ -2508,7 +2635,11 @@ int igoto(int place) } else if (!visual) { int prompt_len = prompt ? prompt->len % Wrap : 0; +#if WIDECHAR + ix += diff_col; +#else ix += diff; +#endif if (ix-1 < prompt_len) { /* off left edge of screen/prompt */ if (expnonvis && insert_char && prompt_len - (ix-1) <= Wrap/2) { /* can scroll, and amount of scroll needed is <= half screen */ @@ -2518,11 +2649,22 @@ int igoto(int place) hwrite(prompt, prompt->len - prompt_len, prompt_len, 0); n = prompt_len - (ix-1); /* get ix onto screen */ if (sidescroll > n) n = sidescroll; /* bring up to minimum */ +#if WIDECHAR + /* Don't scroll more columns than exist to the left of the cursor. */ + { int maxcol = old_col; + if (n > maxcol) n = maxcol; } +#else if (n > keyboard_pos-diff) n = keyboard_pos-diff; /* too far? */ +#endif for (i = 0; i < n; i++) tp(insert_char); ix += i; +#if WIDECHAR + ictrl_put(keybuf->data + keyboard_pos, + utf8_bytes_for_columns(keybuf->data + keyboard_pos, keybuf->len - keyboard_pos, i)); +#else ictrl_put(keybuf->data + keyboard_pos + prompt_len - (ix-1), i); +#endif bufputnc('\010', (prompt_len + i) - (ix - 1)); cx = ix; cy = lines; @@ -2535,8 +2677,15 @@ int igoto(int place) physical_refresh(); } else { /* amount of scroll needed is <= half screen */ - int offset = place - ix + iendx; int i; + int offset; +#if WIDECHAR + int col_place = utf8_byte_to_column(keybuf->data, keybuf->len, place); + int start_col = col_place - (ix - 1) + (iendx - 1); + offset = utf8_column_to_byte(keybuf->data, keybuf->len, start_col); +#else + offset = place - ix + iendx; +#endif bufputc('\r'); if (prompt) hwrite(prompt, prompt->len - prompt_len, prompt_len, 0); @@ -2548,8 +2697,13 @@ int igoto(int place) cy = lines; xy(iendx, lines); ioutputs(keybuf->data + offset, keybuf->len - offset); +#if WIDECHAR + diff_col -= i; + offset = utf8_column_to_byte(keybuf->data, keybuf->len, utf8_byte_to_column(keybuf->data, keybuf->len, offset) + i); +#else diff -= i; offset += i; +#endif xy(ix, lines); } } else { @@ -2557,22 +2711,41 @@ int igoto(int place) physical_refresh(); } } else { /* on screen */ +#if WIDECHAR + cx += diff_col; + if (diff < 0) + bufputnc('\010', diff_col); + else + ictrl_put(keybuf->data + place - diff, diff); +#else cx += diff; if (diff < 0) bufputnc('\010', -diff); else ictrl_put(keybuf->data + place - diff, diff); +#endif } /* visual */ } else { +#if WIDECHAR + int new = (ix - 1) + diff_col; +#else int new = (ix - 1) + diff; +#endif iy += ndiv(new, Wrap); ix = nmod(new, Wrap) + 1; if ((iy > lines) && (iy - lines < isize) && scroll) { scroll_input(iy - lines); +#if WIDECHAR + { int col_place = utf8_byte_to_column(keybuf->data, keybuf->len, place); + int start_col = col_place - (ix - 1) - (iy - lines - 1) * Wrap; + ioutall(utf8_column_to_byte(keybuf->data, keybuf->len, start_col)); + } +#else ioutall(place - (ix - 1) - (iy - lines - 1) * Wrap); +#endif iy = lines; ipos(); } else if ((iy < in_top) || (iy > lines)) { @@ -2607,10 +2780,34 @@ void physical_refresh(void) hwrite(prompt, prompt->len - prompt_len, prompt_len, 0); iendx = prompt_len + 1; } +#if WIDECHAR + if (sockecho() && keybuf->len > 0) { + int col = utf8_byte_to_column(keybuf->data, keybuf->len, keyboard_pos); + int vis_cols = Wrap - prompt_len; + ix = col % vis_cols + 1 + prompt_len; + start = utf8_column_to_byte(keybuf->data, keybuf->len, col - (ix - 1) + prompt_len); + } else { + ix = 1 + prompt_len; + start = 0; + } +#else ix = (sockecho()?keyboard_pos:0) % (Wrap - prompt_len) + 1 + prompt_len; start = (sockecho()?keyboard_pos:0) - (ix - 1) + prompt_len; +#endif if (start == keybuf->len && keybuf->len > 0) { /* would print nothing */ /* slide window so something is visible */ +#if WIDECHAR + { int total_cols = utf8_byte_to_column(keybuf->data, keybuf->len, keybuf->len); + int vis_cols = Wrap - prompt_len; + if (total_cols > vis_cols) { + start = utf8_column_to_byte(keybuf->data, keybuf->len, total_cols - vis_cols); + ix = Wrap + 1; + } else { + start = 0; + ix = total_cols + 1 + prompt_len; + } + } +#else if (start > Wrap - prompt_len) { ix += Wrap - prompt_len; start -= Wrap - prompt_len; @@ -2618,6 +2815,7 @@ void physical_refresh(void) ix += start; start = 0; } +#endif } ioutall(start); bufputnc('\010', iendx - ix); cx -= (iendx - ix); @@ -2630,16 +2828,28 @@ void physical_refresh(void) void logical_refresh(void) { int kpos, nix, niy; +#if WIDECHAR + int col; +#endif if (!visual) oflush(); /* no sense refreshing if there's going to be output after */ kpos = prompt ? -(prompt->len % Wrap) : 0; +#if WIDECHAR + col = sockecho() ? utf8_byte_to_column(keybuf->data, keybuf->len, keyboard_pos) : 0; + nix = (col - kpos) % Wrap + 1; +#else nix = ((sockecho() ? keyboard_pos : 0) - kpos) % Wrap + 1; +#endif if (visual) { setscroll(1, lines); +#if WIDECHAR + niy = istarty + (col - kpos) / Wrap; +#else niy = istarty + (keyboard_pos - kpos) / Wrap; +#endif if (niy <= lines) { clear_input_line(); } else { @@ -2647,7 +2857,11 @@ void logical_refresh(void) kpos += (niy - lines) * Wrap; niy = lines; } +#if WIDECHAR + ioutall(kpos < 0 ? kpos : utf8_column_to_byte(keybuf->data, keybuf->len, kpos)); +#else ioutall(kpos); +#endif ix = nix; iy = niy; ipos(); @@ -2660,6 +2874,27 @@ void logical_refresh(void) hwrite(prompt, prompt->len - plen, plen, 0); iendx = plen + 1; } +#if WIDECHAR + if (sockecho() && keybuf->len > 0) { + col = utf8_byte_to_column(keybuf->data, keybuf->len, keyboard_pos); + ix = (old_ix >= iendx && old_ix <= Wrap) ? old_ix : col % (Wrap - plen) + 1 + plen; + kpos = utf8_column_to_byte(keybuf->data, keybuf->len, col - (ix - 1) + plen); + } else { + ix = 1 + plen; + kpos = 0; + } + old_ix = -1; + if (kpos == keybuf->len && keybuf->len > 0) { + int total_cols = utf8_byte_to_column(keybuf->data, keybuf->len, keybuf->len); + if (total_cols > Wrap/2) { + ix += Wrap/2; + kpos = utf8_column_to_byte(keybuf->data, keybuf->len, total_cols - Wrap/2); + } else { + ix += total_cols; + kpos = 0; + } + } +#else ix = (old_ix >= iendx && old_ix <= Wrap) ? old_ix : (sockecho()?keyboard_pos:0) % (Wrap - plen) + 1 + plen; old_ix = -1; /* invalid */ @@ -2674,16 +2909,26 @@ void logical_refresh(void) kpos -= kpos; } } +#endif ioutall(kpos); } else { ioutall(kpos); kpos += Wrap; +#if WIDECHAR + while ((sockecho() && kpos <= col) || kpos < 0) { + crnl(1); cx = 1; + iendx = 1; + ioutall(kpos < 0 ? kpos : utf8_column_to_byte(keybuf->data, keybuf->len, kpos)); + kpos += Wrap; + } +#else while ((sockecho() && kpos <= keyboard_pos) || kpos < 0) { crnl(1); cx = 1; iendx = 1; ioutall(kpos); kpos += Wrap; } +#endif ix = nix; } bufputnc('\010', iendx - ix); cx -= (iendx - ix); diff --git a/src/util.c b/src/util.c index 95a8826..8b0dcc1 100644 --- a/src/util.c +++ b/src/util.c @@ -270,6 +270,55 @@ int utf8_next_n_chars(const char *s, int len, int pos, int n) return p; } +/* Number of complete UTF-8 characters in the first n bytes at s. */ +int utf8_char_count(const char *s, int n) +{ + int pos = 0, count = 0, next; + + if (!s || n <= 0) return 0; + while (pos < n) { + next = utf8_next_char(s, n, pos); + if (next > n) break; /* character extends past n */ + count++; + pos = next; + } + return count; +} + +/* Column index (0-based, 1 char = 1 column) at byte_offset in s[0..len-1]. */ +int utf8_byte_to_column(const char *s, int len, int byte_offset) +{ + int pos = 0, col = 0; + + if (!s || len <= 0) return 0; + if (byte_offset > len) byte_offset = len; + while (pos < byte_offset) { + pos = utf8_next_char(s, len, pos); + col++; + } + return col; +} + +/* Byte offset after the first maxcol complete characters in s[0..len-1]. */ +int utf8_column_to_byte(const char *s, int len, int maxcol) +{ + int pos = 0, col = 0; + + if (!s || len <= 0) return 0; + if (maxcol <= 0) return 0; + while (col < maxcol && pos < len) { + pos = utf8_next_char(s, len, pos); + col++; + } + return pos; +} + +/* Number of bytes that span at most maxcol columns (complete chars only). */ +int utf8_bytes_for_columns(const char *s, int len, int maxcol) +{ + return utf8_column_to_byte(s, len, maxcol); +} + /* String handlers * These are heavily used functions, so speed is favored over simplicity. */ diff --git a/src/util.h b/src/util.h index 0f2936a..fea76c6 100644 --- a/src/util.h +++ b/src/util.h @@ -125,6 +125,10 @@ extern int utf8_prev_char(const char *s, int len, int pos); extern int utf8_next_char(const char *s, int len, int pos); extern int utf8_prev_n_chars(const char *s, int len, int pos, int n); extern int utf8_next_n_chars(const char *s, int len, int pos, int n); +extern int utf8_char_count(const char *s, int n); +extern int utf8_byte_to_column(const char *s, int len, int byte_offset); +extern int utf8_column_to_byte(const char *s, int len, int maxcol); +extern int utf8_bytes_for_columns(const char *s, int len, int maxcol); extern type_t string_arithmetic_type(const char *str, int typeset); extern Value *parsenumber(const char *str, const char **caller_endp, diff --git a/tf-lib/kbfunc.tf b/tf-lib/kbfunc.tf index 323a959..b9e9f3a 100644 --- a/tf-lib/kbfunc.tf +++ b/tf-lib/kbfunc.tf @@ -102,14 +102,7 @@ /set insert=%{_old_insert} /def -i kb_transpose_chars = \ - /if ( kbpoint() <= 0 ) /beep 1%; /return 0%; /endif%; \ - /let _old_insert=$[+insert]%;\ - /set insert=0%;\ -; Can't use /dokey_left because it would use %kbnum. - /@test kbgoto(kbpoint() - (kbpoint()==kblen()) - 1)%; \ - /@test input(strcat(substr(kbtail(),1,kbnum>0?kbnum:1), \ - substr(kbtail(),0,1)))%; \ - /set insert=%{_old_insert} + /@test kbtranspose() /def -i kb_last_argument = \ /input $(/last $(/recall -i - -$[1 + (kbnum>0?kbnum:1)])) From 69491999b3881b0526668193bc7279c06181ecca Mon Sep 17 00:00:00 2001 From: Mike Ashton Date: Wed, 11 Feb 2026 02:33:34 +0000 Subject: [PATCH 3/3] Committed by mistake --- src/default | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/default b/src/default index 72fdc3b..b49c54e 120000 --- a/src/default +++ b/src/default @@ -1 +1 @@ -/home/mike/bin/tf \ No newline at end of file +/usr/local/bin/tf \ No newline at end of file