diff options
author | Nicholas Marriott <nicm@cvs.openbsd.org> | 2016-10-11 07:11:41 +0000 |
---|---|---|
committer | Nicholas Marriott <nicm@cvs.openbsd.org> | 2016-10-11 07:11:41 +0000 |
commit | 32374aba9f526a266212ad87350c6c78bfbc7c33 (patch) | |
tree | b9bf3c060ccde9d5603fe04c02938412c6a70905 | |
parent | ee47009059623f31cebd95ff4e755100da541d0e (diff) |
Support UTF-8 entry into the command prompt.
-rw-r--r-- | usr.bin/tmux/status.c | 318 | ||||
-rw-r--r-- | usr.bin/tmux/tmux.h | 6 | ||||
-rw-r--r-- | usr.bin/tmux/utf8.c | 29 |
3 files changed, 233 insertions, 120 deletions
diff --git a/usr.bin/tmux/status.c b/usr.bin/tmux/status.c index 84ebf5bfe53..263116aa1aa 100644 --- a/usr.bin/tmux/status.c +++ b/usr.bin/tmux/status.c @@ -1,4 +1,4 @@ -/* $OpenBSD: status.c,v 1.151 2016/10/10 21:29:23 nicm Exp $ */ +/* $OpenBSD: status.c,v 1.152 2016/10/11 07:11:40 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> @@ -662,18 +662,21 @@ status_prompt_set(struct client *c, const char *msg, const char *input, struct format_tree *ft; int keys; time_t t; + char *tmp; ft = format_create(NULL, 0); format_defaults(ft, c, NULL, NULL, NULL); + t = time(NULL); + tmp = format_expand_time(ft, input, t); status_message_clear(c); status_prompt_clear(c); c->prompt_string = format_expand_time(ft, msg, t); - c->prompt_buffer = format_expand_time(ft, input, t); - c->prompt_index = strlen(c->prompt_buffer); + c->prompt_buffer = utf8_fromcstr(tmp); + c->prompt_index = utf8_strlen(c->prompt_buffer); c->prompt_callbackfn = callbackfn; c->prompt_freefn = freefn; @@ -692,6 +695,7 @@ status_prompt_set(struct client *c, const char *msg, const char *input, c->tty.flags |= (TTY_NOCURSOR|TTY_FREEZE); c->flags |= CLIENT_STATUS; + free(tmp); format_free(ft); } @@ -723,22 +727,26 @@ status_prompt_update(struct client *c, const char *msg, const char *input) { struct format_tree *ft; time_t t; + char *tmp; ft = format_create(NULL, 0); format_defaults(ft, c, NULL, NULL, NULL); + t = time(NULL); + tmp = format_expand_time(ft, input, t); free(c->prompt_string); c->prompt_string = format_expand_time(ft, msg, t); free(c->prompt_buffer); - c->prompt_buffer = format_expand_time(ft, input, t); - c->prompt_index = strlen(c->prompt_buffer); + c->prompt_buffer = utf8_fromcstr(tmp); + c->prompt_index = utf8_strlen(c->prompt_buffer); c->prompt_hindex = 0; c->flags |= CLIENT_STATUS; + free(tmp); format_free(ft); } @@ -746,57 +754,80 @@ status_prompt_update(struct client *c, const char *msg, const char *input) int status_prompt_redraw(struct client *c) { - struct screen_write_ctx ctx; - struct session *s = c->session; - struct screen old_status; - size_t i, size, left, len, off; - struct grid_cell gc; + struct screen_write_ctx ctx; + struct session *s = c->session; + struct screen old_status; + u_int i, offset, left, start, pcursor, pwidth, width; + struct grid_cell gc, cursorgc; if (c->tty.sx == 0 || c->tty.sy == 0) return (0); memcpy(&old_status, &c->status, sizeof old_status); screen_init(&c->status, c->tty.sx, 1, 0); - len = screen_write_strlen("%s", c->prompt_string); - if (len > c->tty.sx) - len = c->tty.sx; - off = 0; - - /* Change colours for command mode. */ if (c->prompt_mdata.mode == 1) style_apply(&gc, s->options, "message-command-style"); else style_apply(&gc, s->options, "message-style"); - screen_write_start(&ctx, NULL, &c->status); + memcpy(&cursorgc, &gc, sizeof cursorgc); + cursorgc.attr ^= GRID_ATTR_REVERSE; + start = screen_write_strlen("%s", c->prompt_string); + if (start > c->tty.sx) + start = c->tty.sx; + + screen_write_start(&ctx, NULL, &c->status); screen_write_cursormove(&ctx, 0, 0); - screen_write_nputs(&ctx, len, &gc, "%s", c->prompt_string); + screen_write_nputs(&ctx, start, &gc, "%s", c->prompt_string); + while (c->status.cx < screen_size_x(&c->status)) + screen_write_putc(&ctx, &gc, ' '); + screen_write_cursormove(&ctx, start, 0); - left = c->tty.sx - len; - if (left != 0) { - size = screen_write_strlen("%s", c->prompt_buffer); - if (c->prompt_index >= left) { - off = c->prompt_index - left + 1; - if (c->prompt_index == size) - left--; - size = left; + left = c->tty.sx - start; + if (left == 0) + goto finished; + + pcursor = utf8_strwidth(c->prompt_buffer, c->prompt_index); + pwidth = utf8_strwidth(c->prompt_buffer, -1); + if (pcursor >= left) { + /* + * The cursor would be outside the screen so start drawing + * with it on the right. + */ + offset = (pcursor - left) + 1; + pwidth = left; + } else + offset = 0; + if (pwidth > left) + pwidth = left; + + width = 0; + for (i = 0; c->prompt_buffer[i].size != 0; i++) { + if (width < offset) { + width += c->prompt_buffer[i].width; + continue; } - screen_write_nputs(&ctx, left, &gc, "%s", c->prompt_buffer + - off); + if (width >= offset + pwidth) + break; + width += c->prompt_buffer[i].width; + if (width > offset + pwidth) + break; - for (i = len + size; i < c->tty.sx; i++) - screen_write_putc(&ctx, &gc, ' '); + if (i != c->prompt_index) { + utf8_copy(&gc.data, &c->prompt_buffer[i]); + screen_write_cell(&ctx, &gc); + } else { + utf8_copy(&cursorgc.data, &c->prompt_buffer[i]); + screen_write_cell(&ctx, &cursorgc); + } } + if (c->status.cx < screen_size_x(&c->status) && c->prompt_index >= i) + screen_write_putc(&ctx, &cursorgc, ' '); +finished: screen_write_stop(&ctx); - /* Apply fake cursor. */ - off = len + c->prompt_index - off; - grid_view_get_cell(c->status.grid, off, 0, &gc); - gc.attr ^= GRID_ATTR_REVERSE; - grid_view_set_cell(c->status.grid, off, 0, &gc); - if (grid_compare(c->status.grid, old_status.grid) == 0) { screen_free(&old_status); return (0); @@ -805,19 +836,37 @@ status_prompt_redraw(struct client *c) return (1); } +/* Is this a separator? */ +static int +status_prompt_in_list(const char *ws, const struct utf8_data *ud) +{ + if (ud->size != 1 || ud->width != 1) + return (0); + return (strchr(ws, *ud->data) != NULL); +} + +/* Is this a space? */ +static int +status_prompt_space(const struct utf8_data *ud) +{ + if (ud->size != 1 || ud->width != 1) + return (0); + return (*ud->data == ' '); +} + /* Handle keys in prompt. */ void status_prompt_key(struct client *c, key_code key) { - struct session *sess = c->session; - struct options *oo = sess->options; + struct options *oo = c->session->options; struct paste_buffer *pb; - char *s, *first, *last, word[64], swapc; - const char *histstr, *bufdata, *wsep = NULL; + char *s, word[64]; + const char *histstr, *bufdata, *ws = NULL; u_char ch; - size_t size, n, off, idx, bufsize; + size_t size, n, off, idx, bufsize, used; + struct utf8_data tmp, *first, *last, *ud; - size = strlen(c->prompt_buffer); + size = utf8_strlen(c->prompt_buffer); switch (mode_key_lookup(&c->prompt_mdata, key, NULL, NULL)) { case MODEKEYEDIT_CURSORLEFT: if (c->prompt_index > 0) { @@ -856,7 +905,7 @@ status_prompt_key(struct client *c, key_code key) } break; case MODEKEYEDIT_COMPLETE: - if (*c->prompt_buffer == '\0') + if (c->prompt_buffer[0].size == 0) break; idx = c->prompt_index; @@ -864,40 +913,50 @@ status_prompt_key(struct client *c, key_code key) idx--; /* Find the word we are in. */ - first = c->prompt_buffer + idx; - while (first > c->prompt_buffer && *first != ' ') + first = &c->prompt_buffer[idx]; + while (first > c->prompt_buffer && !status_prompt_space(first)) first--; - while (*first == ' ') + while (first->size != 0 && status_prompt_space(first)) first++; - last = c->prompt_buffer + idx; - while (*last != '\0' && *last != ' ') + last = &c->prompt_buffer[idx]; + while (last->size != 0 && !status_prompt_space(last)) last++; - while (*last == ' ') + while (last > c->prompt_buffer && status_prompt_space(last)) last--; - if (*last != '\0') + if (last->size != 0) last++; - if (last <= first || - ((size_t) (last - first)) > (sizeof word) - 1) + if (last <= first) break; - memcpy(word, first, last - first); - word[last - first] = '\0'; + + used = 0; + for (ud = first; ud < last; ud++) { + if (used + ud->size >= sizeof word) + break; + memcpy(word + used, ud->data, ud->size); + used += ud->size; + } + if (ud != last) + break; + word[used] = '\0'; /* And try to complete it. */ - if ((s = status_prompt_complete(sess, word)) == NULL) + if ((s = status_prompt_complete(c->session, word)) == NULL) break; /* Trim out word. */ n = size - (last - c->prompt_buffer) + 1; /* with \0 */ - memmove(first, last, n); + memmove(first, last, n * sizeof *c->prompt_buffer); size -= last - first; /* Insert the new word. */ size += strlen(s); off = first - c->prompt_buffer; - c->prompt_buffer = xrealloc(c->prompt_buffer, size + 1); + c->prompt_buffer = xreallocarray(c->prompt_buffer, size + 1, + sizeof *c->prompt_buffer); first = c->prompt_buffer + off; - memmove(first + strlen(s), first, n); - memcpy(first, s, strlen(s)); + memmove(first + strlen(s), first, n * sizeof *c->prompt_buffer); + for (idx = 0; idx < strlen(s); idx++) + utf8_set(&first[idx], s[idx]); c->prompt_index = (first - c->prompt_buffer) + strlen(s); free(s); @@ -907,11 +966,12 @@ status_prompt_key(struct client *c, key_code key) case MODEKEYEDIT_BACKSPACE: if (c->prompt_index != 0) { if (c->prompt_index == size) - c->prompt_buffer[--c->prompt_index] = '\0'; + c->prompt_buffer[--c->prompt_index].size = 0; else { memmove(c->prompt_buffer + c->prompt_index - 1, c->prompt_buffer + c->prompt_index, - size + 1 - c->prompt_index); + (size + 1 - c->prompt_index) * + sizeof *c->prompt_buffer); c->prompt_index--; } c->flags |= CLIENT_STATUS; @@ -922,38 +982,39 @@ status_prompt_key(struct client *c, key_code key) if (c->prompt_index != size) { memmove(c->prompt_buffer + c->prompt_index, c->prompt_buffer + c->prompt_index + 1, - size + 1 - c->prompt_index); + (size + 1 - c->prompt_index) * + sizeof *c->prompt_buffer); c->flags |= CLIENT_STATUS; } break; case MODEKEYEDIT_DELETELINE: case MODEKEYEDIT_SWITCHMODESUBSTITUTELINE: - *c->prompt_buffer = '\0'; + c->prompt_buffer[0].size = 0; c->prompt_index = 0; c->flags |= CLIENT_STATUS; break; case MODEKEYEDIT_DELETETOENDOFLINE: case MODEKEYEDIT_SWITCHMODECHANGELINE: if (c->prompt_index < size) { - c->prompt_buffer[c->prompt_index] = '\0'; + c->prompt_buffer[c->prompt_index].size = 0; c->flags |= CLIENT_STATUS; } break; case MODEKEYEDIT_DELETEWORD: - wsep = options_get_string(oo, "word-separators"); + ws = options_get_string(oo, "word-separators"); idx = c->prompt_index; /* Find a non-separator. */ while (idx != 0) { idx--; - if (!strchr(wsep, c->prompt_buffer[idx])) + if (!status_prompt_in_list(ws, &c->prompt_buffer[idx])) break; } /* Find the separator at the beginning of the word. */ while (idx != 0) { idx--; - if (strchr(wsep, c->prompt_buffer[idx])) { + if (status_prompt_in_list(ws, &c->prompt_buffer[idx])) { /* Go back to the word. */ idx++; break; @@ -962,53 +1023,55 @@ status_prompt_key(struct client *c, key_code key) memmove(c->prompt_buffer + idx, c->prompt_buffer + c->prompt_index, - size + 1 - c->prompt_index); + (size + 1 - c->prompt_index) * + sizeof *c->prompt_buffer); memset(c->prompt_buffer + size - (c->prompt_index - idx), - '\0', c->prompt_index - idx); + '\0', (c->prompt_index - idx) * sizeof *c->prompt_buffer); c->prompt_index = idx; + c->flags |= CLIENT_STATUS; break; case MODEKEYEDIT_NEXTSPACE: - wsep = " "; + ws = " "; /* FALLTHROUGH */ case MODEKEYEDIT_NEXTWORD: - if (wsep == NULL) - wsep = options_get_string(oo, "word-separators"); + if (ws == NULL) + ws = options_get_string(oo, "word-separators"); /* Find a separator. */ while (c->prompt_index != size) { - c->prompt_index++; - if (strchr(wsep, c->prompt_buffer[c->prompt_index])) + idx = ++c->prompt_index; + if (status_prompt_in_list(ws, &c->prompt_buffer[idx])) break; } - /* Find the word right after the separation. */ + /* Find the word right after the separator. */ while (c->prompt_index != size) { - c->prompt_index++; - if (!strchr(wsep, c->prompt_buffer[c->prompt_index])) + idx = ++c->prompt_index; + if (!status_prompt_in_list(ws, &c->prompt_buffer[idx])) break; } c->flags |= CLIENT_STATUS; break; case MODEKEYEDIT_NEXTSPACEEND: - wsep = " "; + ws = " "; /* FALLTHROUGH */ case MODEKEYEDIT_NEXTWORDEND: - if (wsep == NULL) - wsep = options_get_string(oo, "word-separators"); + if (ws == NULL) + ws = options_get_string(oo, "word-separators"); /* Find a word. */ while (c->prompt_index != size) { - c->prompt_index++; - if (!strchr(wsep, c->prompt_buffer[c->prompt_index])) + idx = ++c->prompt_index; + if (!status_prompt_in_list(ws, &c->prompt_buffer[idx])) break; } /* Find the separator at the end of the word. */ while (c->prompt_index != size) { - c->prompt_index++; - if (strchr(wsep, c->prompt_buffer[c->prompt_index])) + idx = ++c->prompt_index; + if (status_prompt_in_list(ws, &c->prompt_buffer[idx])) break; } @@ -1020,23 +1083,23 @@ status_prompt_key(struct client *c, key_code key) c->flags |= CLIENT_STATUS; break; case MODEKEYEDIT_PREVIOUSSPACE: - wsep = " "; + ws = " "; /* FALLTHROUGH */ case MODEKEYEDIT_PREVIOUSWORD: - if (wsep == NULL) - wsep = options_get_string(oo, "word-separators"); + if (ws == NULL) + ws = options_get_string(oo, "word-separators"); /* Find a non-separator. */ while (c->prompt_index != 0) { - c->prompt_index--; - if (!strchr(wsep, c->prompt_buffer[c->prompt_index])) + idx = --c->prompt_index; + if (!status_prompt_in_list(ws, &c->prompt_buffer[idx])) break; } /* Find the separator at the beginning of the word. */ while (c->prompt_index != 0) { - c->prompt_index--; - if (strchr(wsep, c->prompt_buffer[c->prompt_index])) { + idx = --c->prompt_index; + if (status_prompt_in_list(ws, &c->prompt_buffer[idx])) { /* Go back to the word. */ c->prompt_index++; break; @@ -1050,8 +1113,8 @@ status_prompt_key(struct client *c, key_code key) if (histstr == NULL) break; free(c->prompt_buffer); - c->prompt_buffer = xstrdup(histstr); - c->prompt_index = strlen(c->prompt_buffer); + c->prompt_buffer = utf8_fromcstr(histstr); + c->prompt_index = utf8_strlen(c->prompt_buffer); c->flags |= CLIENT_STATUS; break; case MODEKEYEDIT_HISTORYDOWN: @@ -1059,8 +1122,8 @@ status_prompt_key(struct client *c, key_code key) if (histstr == NULL) break; free(c->prompt_buffer); - c->prompt_buffer = xstrdup(histstr); - c->prompt_index = strlen(c->prompt_buffer); + c->prompt_buffer = utf8_fromcstr(histstr); + c->prompt_index = utf8_strlen(c->prompt_buffer); c->flags |= CLIENT_STATUS; break; case MODEKEYEDIT_PASTE: @@ -1069,20 +1132,28 @@ status_prompt_key(struct client *c, key_code key) bufdata = paste_buffer_data(pb, &bufsize); for (n = 0; n < bufsize; n++) { ch = (u_char)bufdata[n]; - if (ch < 32 || ch == 127) + if (ch < 32 || ch >= 127) break; } - c->prompt_buffer = xrealloc(c->prompt_buffer, size + n + 1); + c->prompt_buffer = xreallocarray(c->prompt_buffer, size + n + 1, + sizeof *c->prompt_buffer); if (c->prompt_index == size) { - memcpy(c->prompt_buffer + c->prompt_index, bufdata, n); + for (idx = 0; idx < n; idx++) { + ud = &c->prompt_buffer[c->prompt_index + idx]; + utf8_set(ud, bufdata[idx]); + } c->prompt_index += n; - c->prompt_buffer[c->prompt_index] = '\0'; + c->prompt_buffer[c->prompt_index].size = 0; } else { memmove(c->prompt_buffer + c->prompt_index + n, c->prompt_buffer + c->prompt_index, - size + 1 - c->prompt_index); - memcpy(c->prompt_buffer + c->prompt_index, bufdata, n); + (size + 1 - c->prompt_index) * + sizeof *c->prompt_buffer); + for (idx = 0; idx < n; idx++) { + ud = &c->prompt_buffer[c->prompt_index + idx]; + utf8_set(ud, bufdata[idx]); + } c->prompt_index += n; } @@ -1093,42 +1164,55 @@ status_prompt_key(struct client *c, key_code key) if (idx < size) idx++; if (idx >= 2) { - swapc = c->prompt_buffer[idx - 2]; - c->prompt_buffer[idx - 2] = c->prompt_buffer[idx - 1]; - c->prompt_buffer[idx - 1] = swapc; + utf8_copy(&tmp, &c->prompt_buffer[idx - 2]); + utf8_copy(&c->prompt_buffer[idx - 2], + &c->prompt_buffer[idx - 1]); + utf8_copy(&c->prompt_buffer[idx - 1], &tmp); c->prompt_index = idx; c->flags |= CLIENT_STATUS; } break; case MODEKEYEDIT_ENTER: - if (*c->prompt_buffer != '\0') - status_prompt_add_history(c->prompt_buffer); - if (c->prompt_callbackfn(c->prompt_data, c->prompt_buffer) == 0) + s = utf8_tocstr(c->prompt_buffer); + if (*s != '\0') + status_prompt_add_history(s); + if (c->prompt_callbackfn(c->prompt_data, s) == 0) status_prompt_clear(c); + free(s); break; case MODEKEYEDIT_CANCEL: if (c->prompt_callbackfn(c->prompt_data, NULL) == 0) status_prompt_clear(c); break; case MODEKEY_OTHER: - if (key <= 0x1f || key >= 0x7f) + if (key <= 0x1f || key >= KEYC_BASE) + break; + if (utf8_split(key, &tmp) != UTF8_DONE) break; - c->prompt_buffer = xrealloc(c->prompt_buffer, size + 2); + + c->prompt_buffer = xreallocarray(c->prompt_buffer, size + 2, + sizeof *c->prompt_buffer); if (c->prompt_index == size) { - c->prompt_buffer[c->prompt_index++] = key; - c->prompt_buffer[c->prompt_index] = '\0'; + utf8_copy(&c->prompt_buffer[c->prompt_index], &tmp); + c->prompt_index++; + c->prompt_buffer[c->prompt_index].size = 0; } else { memmove(c->prompt_buffer + c->prompt_index + 1, c->prompt_buffer + c->prompt_index, - size + 1 - c->prompt_index); - c->prompt_buffer[c->prompt_index++] = key; + (size + 1 - c->prompt_index) * + sizeof *c->prompt_buffer); + utf8_copy(&c->prompt_buffer[c->prompt_index], &tmp); + c->prompt_index++; } if (c->prompt_flags & PROMPT_SINGLE) { - if (c->prompt_callbackfn(c->prompt_data, - c->prompt_buffer) == 0) + s = utf8_tocstr(c->prompt_buffer); + if (strlen(s) != 1) + status_prompt_clear(c); + else if (c->prompt_callbackfn(c->prompt_data, s) == 0) status_prompt_clear(c); + free(s); } c->flags |= CLIENT_STATUS; @@ -1247,7 +1331,7 @@ status_prompt_complete_prefix(const char **list, u_int size) /* Complete word. */ static char * -status_prompt_complete(struct session *sess, const char *s) +status_prompt_complete(struct session *session, const char *s) { const char **list = NULL, *colon; u_int size = 0, i; @@ -1300,7 +1384,7 @@ status_prompt_complete(struct session *sess, const char *s) colon = ""; if (*s == ':') { - RB_FOREACH(wl, winlinks, &sess->windows) { + RB_FOREACH(wl, winlinks, &session->windows) { xasprintf(&tmp, ":%s", wl->window->name); if (strncmp(tmp, s, strlen(s)) == 0){ list = xreallocarray(list, size + 1, diff --git a/usr.bin/tmux/tmux.h b/usr.bin/tmux/tmux.h index 65e86dfae6a..4a615ba1e20 100644 --- a/usr.bin/tmux/tmux.h +++ b/usr.bin/tmux/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.650 2016/10/10 17:28:30 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.651 2016/10/11 07:11:40 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> @@ -1300,7 +1300,7 @@ struct client { TAILQ_HEAD(, message_entry) message_log; char *prompt_string; - char *prompt_buffer; + struct utf8_data *prompt_buffer; size_t prompt_index; int (*prompt_callbackfn)(void *, const char *); void (*prompt_freefn)(void *); @@ -2345,6 +2345,8 @@ enum utf8_state utf8_combine(const struct utf8_data *, wchar_t *); enum utf8_state utf8_split(wchar_t, struct utf8_data *); int utf8_strvis(char *, const char *, size_t, int); char *utf8_sanitize(const char *); +size_t utf8_strlen(const struct utf8_data *); +u_int utf8_strwidth(const struct utf8_data *, ssize_t); struct utf8_data *utf8_fromcstr(const char *); char *utf8_tocstr(struct utf8_data *); u_int utf8_cstrwidth(const char *); diff --git a/usr.bin/tmux/utf8.c b/usr.bin/tmux/utf8.c index cf7c85419ba..4f0ac8afe8e 100644 --- a/usr.bin/tmux/utf8.c +++ b/usr.bin/tmux/utf8.c @@ -1,4 +1,4 @@ -/* $OpenBSD: utf8.c,v 1.33 2016/05/27 22:57:27 nicm Exp $ */ +/* $OpenBSD: utf8.c,v 1.34 2016/10/11 07:11:40 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com> @@ -236,6 +236,33 @@ utf8_sanitize(const char *src) return (dst); } +/* Get UTF-8 buffer length. */ +size_t +utf8_strlen(const struct utf8_data *s) +{ + size_t i; + + for (i = 0; s[i].size != 0; i++) + /* nothing */; + return (i); +} + +/* Get UTF-8 string width. */ +u_int +utf8_strwidth(const struct utf8_data *s, ssize_t n) +{ + ssize_t i; + u_int width; + + width = 0; + for (i = 0; s[i].size != 0; i++) { + if (n != -1 && n == i) + break; + width += s[i].width; + } + return (width); +} + /* * Convert a string into a buffer of UTF-8 characters. Terminated by size == 0. * Caller frees. |