hyx

Minimalist but powerful terminal hex editor
git clone https://git.sinitax.com/yx7/hyx
Log | Files | Refs | sfeed.txt

input.c (23016B)


      1
      2#include "common.h"
      3#include "blob.h"
      4#include "view.h"
      5#include "input.h"
      6
      7#include <stdlib.h>
      8#include <stdio.h>
      9#include <string.h>
     10#include <errno.h>
     11#include <ctype.h>
     12#include <setjmp.h>
     13
     14#include <time.h>
     15#include <sys/time.h>
     16
     17extern jmp_buf jmp_mainloop; /* hyx.c */
     18
     19void input_init(struct input *input, struct view *view)
     20{
     21    memset(input, 0, sizeof(*input));
     22    input->view = view;
     23}
     24
     25void input_free(struct input *input)
     26{
     27    free(input->search.needle);
     28}
     29
     30/*
     31 * The C standard forbids assigning arbitrary integers to an enum,
     32 * hence we do it the other way round: assign enumerated constants
     33 * to a general integer type.
     34 */
     35typedef uint16_t key;
     36enum {
     37    KEY_INTERRUPTED = 0x1000,
     38    KEY_SPECIAL_ESCAPE,
     39    KEY_SPECIAL_DELETE,
     40    KEY_SPECIAL_UP, KEY_SPECIAL_DOWN, KEY_SPECIAL_RIGHT, KEY_SPECIAL_LEFT,
     41    KEY_SPECIAL_PGUP, KEY_SPECIAL_PGDOWN,
     42    KEY_SPECIAL_HOME, KEY_SPECIAL_END,
     43};
     44
     45static key getch()
     46{
     47    int c;
     48    errno = 0;
     49    if (EOF == (c = getc(stdin))) {
     50        if (errno == EINTR)
     51            return KEY_INTERRUPTED;
     52        pdie("getc");
     53    }
     54    return c;
     55}
     56
     57static void ungetch(int c)
     58{
     59    if (c != ungetc(c, stdin))
     60        pdie("ungetc");
     61}
     62
     63static key get_key()
     64{
     65    static const struct itimerval timeout = {{0}, {CONFIG_WAIT_ESCAPE / 1000000, CONFIG_WAIT_ESCAPE % 1000000}};
     66    static const struct itimerval stop = {0};
     67
     68    /* FIXME Perhaps it'd be easier to just memorize everything after an escape
     69     * key until the timeout hits and parse it once we got the whole sequence? */
     70    static enum {
     71        none,
     72        discard,
     73        have_escape,
     74        have_bracket,
     75        need_tilde,
     76    } state = none;
     77    static key r;
     78    static uint64_t tick;
     79
     80    key k;
     81
     82    /* If we already saw an escape key and we're at the beginning of the
     83     * function again, it is likely we were interrupted by the timer.
     84     * Check if we've waited long enough for the rest of an escape sequence;
     85     * if yes, we either return the escape key or stop discarding input. */
     86    if ((state == have_escape || state == discard)
     87            && monotonic_microtime() - tick >= CONFIG_WAIT_ESCAPE) {
     88        switch (state) {
     89        case have_escape:
     90            state = none;
     91            r = KEY_SPECIAL_ESCAPE;
     92            goto stop_timer;
     93        case discard:
     94            state = none;
     95            if (setitimer(ITIMER_REAL, &stop, NULL))
     96                pdie("setitimer");
     97            break;
     98        default:
     99            die("unexpected state");
    100        }
    101    }
    102
    103next:
    104
    105    /* This might be a window size change or a timer interrupt, so we need to
    106     * go up to the main loop.  The state machine is untouched by this; we
    107     * can simply continue where we were as soon as we're called again. */
    108    if ((k = getch()) == KEY_INTERRUPTED)
    109        longjmp(jmp_mainloop, 0);
    110
    111    switch (state) {
    112
    113    case none:
    114        if (k != 0x1b)
    115            return k;
    116
    117        state = have_escape;
    118start_timer:
    119        tick = monotonic_microtime();
    120        if (setitimer(ITIMER_REAL, &timeout, NULL))
    121            pdie("setitimer");
    122        goto next;
    123
    124    case discard:
    125        goto next;
    126
    127    case have_escape:
    128        if (k != '[') {
    129            ungetch(k);
    130            state = none;
    131            r = KEY_SPECIAL_ESCAPE;
    132            goto stop_timer;
    133        }
    134        state = have_bracket;
    135        goto next;
    136
    137    case have_bracket:
    138        switch (k) {
    139        case 'A': state = none; r = KEY_SPECIAL_UP; goto stop_timer;
    140        case 'B': state = none; r = KEY_SPECIAL_DOWN; goto stop_timer;
    141        case 'C': state = none; r = KEY_SPECIAL_RIGHT; goto stop_timer;
    142        case 'D': state = none; r = KEY_SPECIAL_LEFT; goto stop_timer;
    143        case 'F': state = none; r = KEY_SPECIAL_END; goto stop_timer;
    144        case 'H': state = none; r = KEY_SPECIAL_HOME; goto stop_timer;
    145        case '3': state = need_tilde; r = KEY_SPECIAL_DELETE; goto next;
    146        case '5': state = need_tilde; r = KEY_SPECIAL_PGUP; goto next;
    147        case '6': state = need_tilde; r = KEY_SPECIAL_PGDOWN; goto next;
    148        case '7': state = need_tilde; r = KEY_SPECIAL_HOME; goto next;
    149        case '8': state = need_tilde; r = KEY_SPECIAL_END; goto next;
    150        default:
    151discard_sequence:
    152              /* We don't know this one. Enter discarding state and
    153               * wait for all the characters to come in. */
    154              state = discard;
    155              goto start_timer;
    156        }
    157
    158    case need_tilde:
    159        if (k != '~')
    160            goto discard_sequence;
    161        state = none;
    162stop_timer:
    163        setitimer(ITIMER_REAL, &stop, NULL);
    164        return r;
    165    }
    166
    167    __builtin_unreachable();
    168}
    169
    170static void do_reset_soft(struct input *input)
    171{
    172    input->low_nibble = 0;
    173    input->cur_val = 0;
    174}
    175
    176static void toggle_mode_select(struct input *input)
    177{
    178    struct view *V = input->view;
    179    struct blob *B = V->blob;
    180
    181    switch (input->mode) {
    182    case INPUT:
    183        if (!blob_length(B))
    184            break;
    185        input->mode = SELECT;
    186        input->sel = (input->cur -= (input->cur >= blob_length(B)));
    187        view_dirty_at(V, input->cur);
    188        break;
    189    case SELECT:
    190        input->mode = INPUT;
    191        view_dirty_fromto(input->view, input->sel, input->cur + 1);
    192        view_dirty_fromto(input->view, input->cur, input->sel + 1);
    193        break;
    194    }
    195}
    196
    197static size_t cur_bound(struct input const *input)
    198{
    199    size_t bound = blob_length(input->view->blob);
    200    bound += !bound || (input->mode == INPUT && input->input_mode.insert);
    201    assert(bound >= 1);
    202    return bound;
    203}
    204
    205static size_t sat_sub_step(size_t x, size_t y, size_t z, size_t _)
    206{
    207    (void) _; assert(z);
    208    return x >= y * z ? x - y * z : x % z;
    209}
    210
    211static size_t sat_add_step(size_t x, size_t y, size_t z, size_t b)
    212{
    213    assert(z); assert(x < b);
    214    return x + y * z < b ? x + y * z : b - 1 - (b - 1 - x) % z;
    215}
    216
    217enum cur_move_direction { MOVE_LEFT, MOVE_RIGHT };
    218static void cur_move_rel(struct input *input, enum cur_move_direction dir, size_t off, size_t step)
    219{
    220    assert(input->cur <= cur_bound(input));
    221
    222    struct view *V = input->view;
    223
    224    do_reset_soft(input);
    225    view_dirty_at(V, input->cur);
    226    switch (dir) {
    227    case MOVE_LEFT: input->cur = sat_sub_step(input->cur, off, step, cur_bound(input)); break;
    228    case MOVE_RIGHT: input->cur = sat_add_step(input->cur, off, step, cur_bound(input)); break;
    229    default: die("unexpected direction");
    230    }
    231    assert(input->cur < cur_bound(input));
    232    view_dirty_at(V, input->cur);
    233    view_adjust(V);
    234}
    235
    236static void cur_adjust(struct input *input)
    237{
    238    struct view *V = input->view;
    239
    240    do_reset_soft(input);
    241    if (input->cur >= cur_bound(input)) {
    242        view_dirty_at(V, input->cur);
    243        input->cur = cur_bound(input) - 1;
    244        view_dirty_at(V, input->cur);
    245        view_adjust(V);
    246    }
    247}
    248
    249static void do_reset_hard(struct input *input)
    250{
    251    if (input->mode == SELECT)
    252        toggle_mode_select(input);
    253    input->input_mode.insert = input->input_mode.ascii = false;
    254    cur_adjust(input);
    255    view_dirty_at(input->view, input->cur);
    256}
    257
    258static void toggle_mode_insert(struct input *input)
    259{
    260    struct view *V = input->view;
    261
    262    if (!blob_can_move(V->blob)) {
    263        view_error(V, "can't insert: file is memory-mapped.");
    264        return;
    265    }
    266
    267    if (input->mode != INPUT)
    268        return;
    269    input->input_mode.insert = !input->input_mode.insert;
    270    cur_adjust(input);
    271    view_dirty_at(V, input->cur);
    272}
    273
    274static void toggle_mode_ascii(struct input *input)
    275{
    276    struct view *V = input->view;
    277
    278    if (input->mode != INPUT)
    279        return;
    280    input->input_mode.ascii = !input->input_mode.ascii;
    281    view_dirty_at(V, input->cur);
    282}
    283
    284static void do_yank(struct input *input)
    285{
    286    switch (input->mode) {
    287    case INPUT:
    288        input->sel = input->cur;
    289        input->mode = SELECT;
    290        /* fall-through */
    291    case SELECT:
    292        blob_yank(input->view->blob, min(input->sel, input->cur), absdiff(input->sel, input->cur) + 1);
    293        toggle_mode_select(input);
    294    }
    295}
    296
    297static size_t do_paste(struct input *input)
    298{
    299    struct view *V = input->view;
    300    struct blob *B = V->blob;
    301    size_t retval;
    302
    303    if (input->mode != INPUT)
    304        return 0;
    305    view_adjust(input->view);
    306    do_reset_soft(input);
    307    retval = blob_paste(B, input->cur, input->input_mode.insert ? INSERT : REPLACE);
    308    view_recompute(V, false);
    309    if (input->input_mode.insert)
    310        view_dirty_from(input->view, input->cur);
    311    else
    312        view_dirty_fromto(input->view, input->cur, input->cur + input->view->blob->clipboard.len);
    313
    314    return retval;
    315}
    316
    317static bool do_delete(struct input *input, bool back)
    318{
    319    struct view *V = input->view;
    320    struct blob *B = V->blob;
    321
    322    if (!blob_can_move(B)) {
    323        view_error(V, "can't delete: file is memory-mapped.");
    324        return false;
    325    }
    326    if (!blob_length(B))
    327        return false;
    328
    329    if (back) {
    330        if (!input->cur)
    331            return false;
    332        cur_move_rel(input, MOVE_LEFT, 1, 1);
    333        if (!input->input_mode.insert)
    334            return true;
    335    }
    336
    337    switch (input->mode) {
    338    case INPUT:
    339        input->mode = SELECT;
    340        cur_adjust(input);
    341        input->sel = input->cur;
    342        /* fall-through */
    343    case SELECT:
    344        do_reset_soft(input);
    345        do_yank(input);
    346        if (input->cur > input->sel) {
    347            size_t tmp = input->cur;
    348            input->cur = input->sel;
    349            input->sel = tmp;
    350        }
    351        blob_delete(B, input->cur, input->sel - input->cur + 1, true);
    352        view_recompute(V, false);
    353        cur_adjust(input);
    354        view_dirty_from(V, input->cur);
    355        view_adjust(V);
    356    }
    357    return true;
    358}
    359
    360static void do_quit(struct input *input, bool *quit, bool force)
    361{
    362    struct view *V = input->view;
    363    if (force || blob_is_saved(V->blob))
    364        *quit = true;
    365    else
    366        view_error(V, "unsaved changes! use :q! if you are sure.");
    367}
    368
    369static void do_search_cont(struct input *input, ssize_t dir)
    370{
    371    struct view *V = input->view;
    372    size_t blen = blob_length(V->blob);
    373
    374    if (!blen)
    375        return;
    376
    377    size_t cur = dir > 0 ? min(input->cur, blen-1) : input->cur;
    378    ssize_t pos = blob_search(V->blob, input->search.needle, input->search.len, (cur + blen + dir) % blen, dir);
    379
    380    if (pos < 0)
    381        return;
    382
    383    view_dirty_at(V, input->cur);
    384    input->cur = pos;
    385    view_dirty_at(V, input->cur);
    386    view_adjust(V);
    387}
    388
    389static void do_inc_dec(struct input *input, byte diff)
    390{
    391    struct view *V = input->view;
    392    struct blob *B = V->blob;
    393
    394    /* should we do anything for selections? */
    395    if (input->mode != INPUT)
    396        return;
    397
    398    if (input->cur >= blob_length(B))
    399        return;
    400
    401    byte b = blob_at(B, input->cur) + diff;
    402    blob_replace(input->view->blob, input->cur, &b, 1, true);
    403    view_dirty_at(V, input->cur);
    404}
    405
    406void do_home_end(struct input *input, size_t soft, size_t hard)
    407{
    408    assert(soft <= cur_bound(input));
    409    assert(hard <= cur_bound(input));
    410
    411    struct view *V = input->view;
    412
    413    do_reset_soft(input);
    414    view_dirty_at(V, input->cur);
    415    input->cur = input->cur == soft ? hard : soft;
    416    view_dirty_at(V, input->cur);
    417    if (input->mode == SELECT)
    418        view_dirty_from(V, 0); /* FIXME suboptimal */
    419    view_adjust(V);
    420}
    421
    422void do_pgup_pgdown(struct input *input, size_t (*f)(size_t, size_t, size_t, size_t))
    423{
    424    struct view *V = input->view;
    425
    426    do_reset_soft(input);
    427    input->cur = f(input->cur, V->rows, V->cols, cur_bound(input));
    428    V->start = f(V->start, V->rows, V->cols, cur_bound(input));
    429    view_dirty_from(V, 0);
    430    view_adjust(V);
    431}
    432
    433
    434void input_cmd(struct input *input, bool *quit);
    435void input_search(struct input *input);
    436
    437void input_get(struct input *input, bool *quit)
    438{
    439    key k;
    440    byte b;
    441
    442    struct view *V = input->view;
    443    struct blob *B = V->blob;
    444
    445    k = get_key();
    446
    447    if (input->mode == INPUT) {
    448
    449        if (input->input_mode.ascii && isprint(k)) {
    450
    451            /* ascii input */
    452
    453            if (!blob_length(B))
    454                input->input_mode.insert = true;
    455
    456            b = k;
    457            if (input->input_mode.insert) {
    458                blob_insert(B, input->cur, &b, sizeof(b), true);
    459                view_recompute(V, false);
    460                view_dirty_from(V, input->cur);
    461            }
    462            else {
    463                blob_replace(B, input->cur, &b, sizeof(b), true);
    464                view_dirty_at(V, input->cur);
    465            }
    466
    467            cur_move_rel(input, MOVE_RIGHT, 1, 1);
    468            return;
    469        }
    470
    471        if ((k >= '0' && k <= '9') || (k >= 'a' && k <= 'f')) {
    472
    473            /* hex input */
    474
    475            if (!blob_length(B))
    476                input->input_mode.insert = true;
    477
    478            if (input->input_mode.insert) {
    479                if (!input->low_nibble)
    480                    input->cur_val = 0;
    481                input->cur_val |= (k > '9' ? k - 'a' + 10 : k - '0') << 4 * (input->low_nibble = !input->low_nibble);
    482                if (input->low_nibble) {
    483                    blob_insert(B, input->cur, &input->cur_val, sizeof(input->cur_val), true);
    484                    view_recompute(V, false);
    485                    view_dirty_from(V, input->cur);
    486                }
    487                else {
    488                    blob_replace(B, input->cur, &input->cur_val, sizeof(input->cur_val), true);
    489                    view_dirty_at(V, input->cur);
    490                    cur_move_rel(input, MOVE_RIGHT, 1, 1);
    491                    return;
    492                }
    493            }
    494            else {
    495                input->cur_val = input->cur < blob_length(B) ? blob_at(B, input->cur) : 0;
    496                input->cur_val = input->cur_val & 0xf << 4 * input->low_nibble;
    497                input->cur_val |= (k > '9' ? k - 'a' + 10 : k - '0') << 4 * (input->low_nibble = !input->low_nibble);
    498                blob_replace(B, input->cur, &input->cur_val, sizeof(input->cur_val), true);
    499                view_dirty_at(V, input->cur);
    500
    501                if (!input->low_nibble) {
    502                    cur_move_rel(input, MOVE_RIGHT, 1, 1);
    503                    return;
    504                }
    505
    506            }
    507
    508            view_adjust(V);
    509            return;
    510        }
    511
    512    }
    513
    514    /* function keys */
    515
    516    switch (k) {
    517
    518    case KEY_SPECIAL_ESCAPE:
    519        do_reset_hard(input);
    520        break;
    521
    522    case 0x7f: /* backspace */
    523        do_delete(input, true);
    524        break;
    525
    526    case 'x':
    527    case KEY_SPECIAL_DELETE:
    528        do_delete(input, false);
    529        break;
    530
    531    case 'q':
    532        do_quit(input, quit, false);
    533        break;
    534
    535    case 'v':
    536        toggle_mode_select(input);
    537        break;
    538
    539    case 'y':
    540        do_yank(input);
    541        break;
    542
    543    case 's':
    544        if (input->mode == SELECT && input->cur > input->sel) {
    545            size_t tmp = input->sel;
    546            input->sel = input->cur;
    547            input->cur = tmp;
    548        }
    549        if (do_delete(input, false) && !input->input_mode.insert)
    550            toggle_mode_insert(input);
    551        break;
    552
    553    case 'p':
    554        do_paste(input);
    555        break;
    556
    557    case 'P':
    558        cur_move_rel(input, MOVE_RIGHT, do_paste(input), 1);
    559        break;
    560
    561    case 'i':
    562        toggle_mode_insert(input);
    563        break;
    564
    565    case '\t':
    566        toggle_mode_ascii(input);
    567        break;
    568
    569    case 'u':
    570        if (input->mode != INPUT) break;
    571        if (!blob_undo(B, &input->cur))
    572            break;
    573        view_recompute(V, false);
    574        cur_adjust(input);
    575        view_adjust(V);
    576        view_dirty_from(V, 0); /* FIXME suboptimal */
    577        break;
    578
    579    case 0x12: /* ctrl + R */
    580        if (input->mode != INPUT) break;
    581        if (!blob_redo(B, &input->cur))
    582            break;
    583        view_recompute(V, false);
    584        cur_adjust(input);
    585        view_adjust(V);
    586        view_dirty_from(V, 0); /* FIXME suboptimal */
    587        break;
    588
    589    case 0x7: /* ctrl + G */
    590        {
    591             char buf[256];
    592             snprintf(buf, sizeof(buf), "\"%s\" %s%s %zd/%zd bytes --%zd%%--",
    593                 input->view->blob->filename,
    594                 input->view->blob->alloc == BLOB_MMAP ? "[mmap]" : "",
    595                 input->view->blob->saved_dist ? "[modified]" : "[saved]",
    596                 input->cur,
    597                 blob_length(input->view->blob),
    598                 ((input->cur+1) * 100) / blob_length(input->view->blob));
    599             view_message(V, buf, NULL);
    600        }
    601        break;
    602
    603    case 0xc: /* ctrl + L */
    604        view_dirty_from(V, 0);
    605        break;
    606
    607    case ':':
    608        printf("\x1b[%uH", V->rows); /* move to last line */
    609        view_text(V, false);
    610        printf(":");
    611        input_cmd(input, quit);
    612        view_dirty_from(V, 0);
    613        view_visual(V);
    614        break;
    615
    616    case '/':
    617        printf("\x1b[%uH", V->rows); /* move to last line */
    618        view_text(V, false);
    619        printf("/");
    620        input_search(input);
    621        view_dirty_from(V, 0);
    622        view_visual(V);
    623        break;
    624
    625    case 'n':
    626        do_search_cont(input, +1);
    627        break;
    628
    629    case 'N':
    630        do_search_cont(input, -1);
    631        break;
    632
    633    case 0x1: /* ctrl + A */
    634        do_inc_dec(input, 1);
    635        break;
    636
    637    case 0x18: /* ctrl + X */
    638        do_inc_dec(input, -1);
    639        break;
    640
    641    case 'j':
    642    case KEY_SPECIAL_DOWN:
    643        cur_move_rel(input, MOVE_RIGHT, 1, V->cols);
    644        break;
    645
    646    case 'k':
    647    case KEY_SPECIAL_UP:
    648        cur_move_rel(input, MOVE_LEFT, 1, V->cols);
    649        break;
    650
    651    case 'l':
    652    case KEY_SPECIAL_RIGHT:
    653        cur_move_rel(input, MOVE_RIGHT, 1, 1);
    654        break;
    655
    656    case 'h':
    657    case KEY_SPECIAL_LEFT:
    658        cur_move_rel(input, MOVE_LEFT, 1, 1);
    659        break;
    660
    661    case '^':
    662        cur_move_rel(input, MOVE_LEFT, (input->cur - V->start) % V->cols, 1);
    663        break;
    664
    665    case '$':
    666        cur_move_rel(input, MOVE_RIGHT, V->cols-1 - (input->cur - V->start) % V->cols, 1);
    667        break;
    668
    669    case 'g':
    670    case KEY_SPECIAL_HOME:
    671        do_home_end(input, min(V->start, cur_bound(input) - 1), 0);
    672        break;
    673
    674    case 'G':
    675    case KEY_SPECIAL_END:
    676        do_home_end(input, min(V->start + V->rows * V->cols - 1, cur_bound(input) - 1), cur_bound(input) - 1);
    677        break;
    678
    679    case 0x15: /* ctrl + U */
    680    case KEY_SPECIAL_PGUP:
    681        do_pgup_pgdown(input, sat_sub_step);
    682        break;
    683
    684    case 0x4: /* ctrl + D */
    685    case KEY_SPECIAL_PGDOWN:
    686        do_pgup_pgdown(input, sat_add_step);
    687        break;
    688
    689    case '[':
    690        view_set_cols(V, true, -1);
    691        break;
    692
    693    case ']':
    694        view_set_cols(V, true, +1);
    695        break;
    696
    697    }
    698}
    699
    700void input_cmd(struct input *input, bool *quit)
    701{
    702    char buf[0x100], *p;
    703    unsigned long long n;
    704
    705    if (!fgets_retry(buf, sizeof(buf), stdin))
    706        pdie("fgets");
    707
    708    if ((p = strchr(buf, '\n')))
    709        *p = 0;
    710
    711    if (!(p = strtok(buf, " ")))
    712        return;
    713    else if (!strcmp(p, "w") || !strcmp(p, "wq")) {
    714        switch (blob_save(input->view->blob, strtok(NULL, " "))) {
    715        case BLOB_SAVE_OK:
    716            if (!strcmp(p, "wq"))
    717                do_quit(input, quit, false);
    718            break;
    719        case BLOB_SAVE_FILENAME:
    720            view_error(input->view, "can't save: no filename.");
    721            break;
    722        case BLOB_SAVE_NONEXISTENT:
    723            view_error(input->view, "can't save: nonexistent path.");
    724            break;
    725        case BLOB_SAVE_PERMISSIONS:
    726            view_error(input->view, "can't save: insufficient permissions.");
    727            break;
    728        case BLOB_SAVE_BUSY:
    729            view_error(input->view, "can't save: file is busy.");
    730            break;
    731        default:
    732            die("can't save: unknown error");
    733        }
    734    }
    735    else if (!strcmp(p, "q") || !strcmp(p, "q!")) {
    736        do_quit(input, quit, !strcmp(p, "q!"));
    737    }
    738    else if (!strcmp(p, "color")) {
    739        if ((p = strtok(NULL, " ")))
    740            input->view->color = *p == '1' || *p == 'y';
    741    }
    742    else if (!strcmp(p, "columns")) {
    743        if ((p = strtok(NULL, " "))) {
    744            if (!strcmp(p, "auto")) {
    745                view_set_cols(input->view, false, 0);
    746            }
    747            else {
    748                n = strtoull(p, &p, 0);
    749                if (!*p)
    750                    view_set_cols(input->view, false, n);
    751            }
    752        }
    753    }
    754    else {
    755        /* try to interpret the input as an offset */
    756        n = strtoull(p, &p, 0);
    757        if (!*p) {
    758            view_dirty_at(input->view, input->cur);
    759            if (n < cur_bound(input))
    760                input->cur = n;
    761            view_dirty_at(input->view, input->cur);
    762            view_adjust(input->view);
    763        }
    764    }
    765}
    766
    767static unsigned unhex_digit(char c)
    768{
    769    assert(isxdigit(c));
    770    if (c >= '0' && c <= '9')
    771        return c - '0';
    772    else if (c >= 'a' && c <= 'f')
    773        return c - 'a' + 10;
    774    else if (c >= 'A' && c <= 'F')
    775        return c - 'A' + 10;
    776    die("not a hex digit");
    777}
    778
    779static size_t unhex(byte **ret, char const *hex)
    780{
    781    size_t len = 0;
    782    *ret = malloc_strict(strlen(hex) / 2);
    783    for (char const *p = hex; *p; ) {
    784        while (isspace(*p)) ++p;
    785        if (!(isxdigit(p[0]) && isxdigit(p[1]))) {
    786            free(*ret);
    787            *ret = NULL;
    788            return 0;
    789        }
    790        (*ret)[len] = unhex_digit(*p++) << 4;
    791        (*ret)[len++] |= unhex_digit(*p++);
    792    }
    793    *ret = realloc_strict(*ret, len); /* shrink to what we actually used */
    794    return len;
    795}
    796
    797/* NB: this accepts some technically invalid inputs */
    798static size_t utf8_to_ucs2(byte **ret, char const *str)
    799{
    800    size_t len = 0;
    801    *ret = malloc_strict(2 * strlen(str));
    802    for (uint32_t c, b; (c = *str++); ) {
    803        if (!(c & 0x80)) b = 0;
    804        else if ((c & 0xe0) == 0xc0) c &= 0x1f, b = 1;
    805        else if ((c & 0xf0) == 0xe0) c &= 0x0f, b = 2;
    806        else if ((c & 0xf8) == 0xf0) c &= 0x07, b = 3;
    807        else {
    808bad:
    809            free(*ret);
    810            *ret = NULL;
    811            return 0;
    812        }
    813        while (b--) {
    814            if ((*str & 0xc0) != 0x80) goto bad;
    815            c <<= 6, c |= (*str++ & 0x3f);
    816        }
    817        if (c >> 16) goto bad; /* not representable */
    818        (*ret)[len++] = c >> 0;
    819        (*ret)[len++] = c >> 8;
    820    }
    821    *ret = realloc_strict(*ret, len); /* shrink to what we actually used */
    822    return len;
    823}
    824
    825void input_search(struct input *input)
    826{
    827    char buf[0x100], *p, *q;
    828
    829    if (!fgets_retry(buf, sizeof(buf), stdin))
    830        pdie("fgets");
    831
    832    if ((p = strchr(buf, '\n')))
    833        *p = 0;
    834
    835    input->search.len = 0;
    836    free(input->search.needle);
    837    input->search.needle = NULL;
    838
    839    if (!(p = strtok(buf, " ")))
    840        return;
    841    else if (!strcmp(p, "x") || !strcmp(p, "w")) {
    842        size_t (*fun)(byte **, char const *) = (*p == 'x') ? unhex : utf8_to_ucs2;
    843        if (!(q = strtok(NULL, " "))) {
    844            q = p;
    845            goto str;
    846        }
    847        input->search.len = fun(&input->search.needle, q);
    848    }
    849    else if (!strcmp(p, "s")) {
    850        if (!(q = strtok(NULL, "")))
    851            q = p;
    852str:
    853        input->search.len = strlen(q);
    854        input->search.needle = (byte *) strdup(q);
    855    }
    856    else if (!(input->search.len = unhex(&input->search.needle, p))) {
    857        q = p;
    858        goto str;
    859    }
    860
    861    do_search_cont(input, +1);
    862}
    863