/*
 * Shell mode for QEmacs.
 *
 * Copyright (c) 2001-2002 Fabrice Bellard.
 * Copyright (c) 2002-2026 Charlie Gordon.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <time.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <termios.h>
#include "qe.h"

/* XXX: status line */
/* XXX: better tab handling */
/* XXX: send real cursor position (CSI n) */

static ModeDef shell_mode, pager_mode, compilation_mode;

#define MAX_CSI_PARAMS  16
#define MAX_OSC_SIZE    512 + MAX_FILENAME_SIZE

enum QETermState {
    QE_TERM_STATE_NORM,
    QE_TERM_STATE_UTF8,
    QE_TERM_STATE_ESC,
    QE_TERM_STATE_ESC2,
    QE_TERM_STATE_CSI,
    QE_TERM_STATE_OSC1,
    QE_TERM_STATE_STRING,
};

typedef struct ShellCwdEntry {
    QEProperty *prop;
    int error_count;
} ShellCwdEntry;

typedef struct ShellState {
    QEModeData base;
    /* buffer state */
    int cols, rows;
    int use_alternate_screen;
    int screen_top, alternate_screen_top;
    int scroll_top, scroll_bottom;  /* scroll region (top included, bottom excluded) */
    int pty_fd;
    int pid; /* -1 if not launched */
    int exit_status; /* -1 if not launched */
    unsigned int attr, fgcolor, bgcolor, reverse;
    int cur_offset; /* current offset at position x, y */
    int cur_offset_hack; /* the target position is in the middle of a wide glyph */
    int cur_prompt; /* offset of end of prompt on current line */
    int save_x, save_y;
    int nb_params;
#define CSI_PARAM_OMITTED  0x80000000
    int params[MAX_CSI_PARAMS + 1];
    int state;
    int esc1, esc2;
    char32_t lastc;
    int shifted;
    int cset, charset[2];
    int grab_keys;      // XXX: should detect raw mode instead of relying on alternate_screen
    int last_key_grabbed;
    unsigned char term_buf[256];
    int term_len, term_pos;
    int utf8_len;
    char osc_buf[MAX_OSC_SIZE];
    int osc_len;
    EditBuffer *b;
    EditBuffer *b_color; /* color buffer, one byte per char */
    const char *ka1, *ka3, *kb2, *kc1, *kc3, *kcbt, *kspd;
    const char *kbeg, *kbs, *kent, *kdch1, *kich1;
    const char *kcub1, *kcud1, *kcuf1, *kcuu1;
    const char *kf1, *kf2, *kf3, *kf4, *kf5;
    const char *kf6, *kf7, *kf8, *kf9, *kf10;
    const char *kf11, *kf12, *kf13, *kf14, *kf15;
    const char *kf16, *kf17, *kf18, *kf19, *kf20;
    const char *khome, *kend, *kmous, *knp, *kpp;
    const char *caption;  /* process caption for exit message */
    int shell_flags;
    int last_char;  /* last char sent to the process */
    int cwd_stack_depth;
    ShellCwdEntry cwd_stack[16];
} ShellState;

typedef struct ShellError {
    int offset;
    int msg_offset;
    int line_num;
    int col_num;
    char buffer[MAX_BUFFERNAME_SIZE];
    char filename[MAX_FILENAME_SIZE];
    char message[128];
} ShellError;

typedef struct ShellKillContext {
    ShellState *s;
    EditBuffer *b;
    void (*callback)(EditState*, const char*);
    EditState *e;
    char *cmd;  /* allocated */
} ShellKillContext;

static ShellError error_state = {
    .offset   = -1,
    .line_num = -1,
    .col_num  = -1,
};

#define SR_UPDATE_SIZE  1
#define SR_REFRESH      2
#define SR_SILENT       4
static void do_shell_refresh(EditState *e, int flags);

static void shell_close(ShellState *s);
static int shell_check_curpath(ShellState *s, int offset, int c);
static int match_error(EditBuffer *b, int start_offset, ShellError *dest);

static void set_error_offset(EditBuffer *b, int offset)
{
    pstrcpy(error_state.buffer, sizeof(error_state.buffer), b ? b->name : "");
    error_state.offset = offset - 1;
    error_state.line_num = error_state.col_num = -1;
    *error_state.filename = '\0';
    error_state.msg_offset = offset;
    *error_state.message = '\0';
}

static QEProperty *shell_add_cwd(EditBuffer *b, int offset, const char *cwd, int force) {
    // Always set the buffer filename for bufed
    pstrcpy(unconst(char *)b->filename, countof(b->filename), cwd);
    if (!force) {
        QEProperty *last = eb_find_property(b, 0, offset, QE_PROP_CWD, NULL);
        if (last && strequal(last->data, cwd))
            return NULL;
    }
    return eb_add_property(b, offset, QE_PROP_CWD,
                           QE_PROP_DUP | QE_PROP_KEEP | QE_PROP_MARK, cwd);
}

#define PTYCHAR1 "pqrstuvwxyzabcde"
#define PTYCHAR2 "0123456789abcdef"

/* allocate one pty/tty pair */
static int get_pty(char *tty_str, int size)
{
    int fd;
    char ptydev[] = "/dev/pty??";
    char ttydev[] = "/dev/tty??";
    int len = strlen(ttydev);
    const char *c1, *c2;

#ifdef CONFIG_PTSNAME
    /* First try Unix98 pseudo tty master */

    /* CG: should check if posix_openpt is more appropriate than /dev/ptmx */
    fd = posix_openpt(O_RDWR | O_NOCTTY);
    //fd = open("/dev/ptmx", O_RDWR);
    if (fd >= 0) {
#if 0
        /* ptsname_r is a sensible renentrant version of ptsname, but
         * it lacks portability, notably on OpenBSD and cygwin. So we
         * have to use ill conceived ptsname.
         */
        if (!ptsname_r(fd, tty_str, size) && !grantpt(fd) && !unlockpt(fd))
            return fd;
#else
        const char *name = ptsname(fd);

        if (name) {
            pstrcpy(tty_str, size, name);
            if (!grantpt(fd) && !unlockpt(fd))
                return fd;
        }
#endif
        close(fd);
    }
#endif
    /* then try BSD pseudo tty pre-created pairs */
    for (c1 = PTYCHAR1; *c1; c1++) {
        ptydev[len-2] = ttydev[len-2] = *c1;
        for (c2 = PTYCHAR2; *c2; c2++) {
            ptydev[len-1] = ttydev[len-1] = *c2;
            if ((fd = open(ptydev, O_RDWR)) >= 0) {
                if (access(ttydev, R_OK|W_OK) == 0) {
                    pstrcpy(tty_str, size, ttydev);
                    return fd;
                }
                close(fd);
            }
        }
    }
    return -1;
}

const char *get_shell(void)
{
    const char *shell_path;

    /* find shell name */
    shell_path = getenv("SHELL");
    if (!shell_path)
        shell_path = "/bin/sh";

    return shell_path;
}

static void qe_closefrom(int fd) {
#if defined(CONFIG_FREEBSD) || defined(CONFIG_OPENBSD)
    closefrom(fd);
#else
    int i, nb_fds = max_int((int)sysconf(_SC_OPEN_MAX), 20);
    for (i = 3; i < nb_fds; i++)
        close(i);
#endif
}

#define QE_TERM_XSIZE  80
#define QE_TERM_YSIZE  25
#define QE_TERM_YSIZE_INFINITE  10000

static int run_process(ShellState *s,
                       const char *cmd, int *fd_ptr, int *pid_ptr,
                       int cols, int rows, const char *path,
                       int shell_flags)
{
    int pty_fd, pid, pipe_fds[2];
    char tty_name[MAX_FILENAME_SIZE];
    char lines_string[20];
    char columns_string[20];

    if (shell_flags & SF_INTERACTIVE) {
        struct winsize ws;

        pty_fd = get_pty(tty_name, sizeof(tty_name));
        if (pty_fd < 0) {
            put_error(s->b->qs->active_window, "run_process: cannot get tty: %s",
                      strerror(errno));
            return -1;
        }
        /* set dummy screen size */
        /* Recommended practice:
           As the winsize structure may have more fields than documented,
           applications planning to call tcsetwinsize() should call
           tcgetwinsize() first with the same fd parameter, and use the
           result obtained in *gws to initialize the winsize structure to
           be used (after altering fields that are to be changed) as the
           sws operand of tcsetwinsize.
         */
        ioctl(pty_fd, TIOCGWINSZ, &ws);
        ws.ws_col = cols;
        ws.ws_row = rows;
        ioctl(pty_fd, TIOCSWINSZ, &ws);
    } else {
        if (pipe(pipe_fds) < 0) {
            put_error(s->b->qs->active_window, "run_process: cannot pipe: %s",
                      strerror(errno));
            return -1;
        }
        pty_fd = pipe_fds[0];
    }

    fcntl(pty_fd, F_SETFL, O_NONBLOCK);

    pid = fork();
    if (pid < 0) {
        put_error(s->b->qs->active_window, "run_process: cannot fork");
        if (shell_flags & SF_INTERACTIVE) {
            close(pty_fd);
        } else {
            close(pipe_fds[0]);
            close(pipe_fds[1]);
        }
        return -1;
    }

    if (pid == 0) {
        /* child process */
        const char *argv[4];
        char qelevel[16];
        char *vp;
        int argc = 0;

        argv[argc++] = get_shell();
        if (cmd) {
            argv[argc++] = "-c";
            argv[argc++] = cmd;
        }
        argv[argc] = NULL;

        /* detach controlling terminal */
#ifndef CONFIG_DARWIN
        setsid();
#endif

        /* open pseudo tty for standard I/O */
        if (shell_flags & SF_INTERACTIVE) {
            /* interactive shell: input from / output to pseudo terminal */
            int tmp_fd = open(tty_name, O_RDWR);
            dup2(tmp_fd, 0);
            dup2(tmp_fd, 1);
            dup2(tmp_fd, 2);
        } else {
            /* collect output from non interactive process: no input */
            // XXX: should be O_RDWR?
            int tmp_fd = open("/dev/null", O_RDONLY);
            dup2(tmp_fd, 0);
            dup2(pipe_fds[1], 1);
            dup2(pipe_fds[1], 2);
        }
        qe_closefrom(3);
#ifdef CONFIG_DARWIN
        setsid();
#endif
        snprintf(lines_string, sizeof lines_string, "%d", rows);
        snprintf(columns_string, sizeof columns_string, "%d", cols);

        setenv("LINES", lines_string, 1);
        setenv("COLUMNS", columns_string, 1);
        setenv("TERM", "xterm-256color", 1);
        setenv("TERM_PROGRAM", "qemacs", 1);
        setenv("TERM_PROGRAM_VERSION", str_version, 1);
        vp = getenv("QELEVEL");
        snprintf(qelevel, sizeof qelevel, "%d", 1 + (vp ? atoi(vp) : 0));
        setenv("QELEVEL", qelevel, 1);

        if (path) {
            if (chdir(path)) {
                setenv("QESTATUS", "cannot chdir", 1);
            }
        }

        execv(argv[0], unconst(char * const *)argv);
        exit(1);
    }

    if (!(shell_flags & SF_INTERACTIVE))
        close(pipe_fds[1]);

    /* return file info */
    *fd_ptr = pty_fd;
    *pid_ptr = pid;
    return 0;
}

/* VT100 emulation */

static void qe_trace_term(ShellState *s, const char *msg) {
    QEmacsState *qs = s->base.qs;
    qe_trace_bytes(qs, msg, -1, EB_TRACE_FLUSH | EB_TRACE_EMULATE);
    qe_trace_bytes(qs, ": ", -1, EB_TRACE_EMULATE);
    qe_trace_bytes(qs, s->term_buf, s->term_len, EB_TRACE_EMULATE);
}

#define TRACE_MSG(s, m)  qe_trace_term(s, m)
#define TRACE_PRINTF(s, ...)  do { \
    if (s->base.qs->trace_buffer) { \
        if (s->base.qs->trace_buffer_state) \
            eb_putc(s->base.qs->trace_buffer, '\n'); \
        eb_printf(s->base.qs->trace_buffer, __VA_ARGS__); \
        s->base.qs->trace_buffer_state = 0; \
    } \
} while(0)

static void qe_term_init(ShellState *s)
{
    char *term;

    s->state = QE_TERM_STATE_NORM;
    /* Should compute def_color from shell default style at display
     * time and force full redisplay upon style change.
     */
    s->fgcolor = QE_TERM_DEF_FG;
    s->bgcolor = QE_TERM_DEF_BG;
    s->attr = 0;
    s->reverse = 0;
    s->lastc = ' ';

    term = getenv("TERM");
    /* vt100 terminfo definitions */
    s->kbs = "\010";
    s->ka1 = "\033Oq";
    s->ka3 = "\033Os";
    s->kb2 = "\033Or";
    s->kc1 = "\033Op";
    s->kc3 = "\033On";
    s->kcub1 = "\033OD";
    s->kcud1 = "\033OB";
    s->kcuf1 = "\033OC";
    s->kcuu1 = "\033OA";
    s->kent = "\033OM";
    s->kf1 = "\033OP";
    s->kf2 = "\033OQ";
    s->kf3 = "\033OR";
    s->kf4 = "\033OS";
    s->kf5 = "\033Ot";
    s->kf6 = "\033Ou";
    s->kf7 = "\033Ov";
    s->kf8 = "\033Ol";
    s->kf9 = "\033Ow";
    s->kf10 = "\033Ox";

    /* ansi terminfo definitions */
    if (strstart(term, "ansi", NULL)) {
        s->kbs = "\010";
        s->kcbt = "\033[Z";
        s->kcub1 = "\033[D";
        s->kcud1 = "\033[B";
        s->kcuf1 = "\033[C";
        s->kcuu1 = "\033[A";
        s->khome = "\033[H";
        s->kich1 = "\033[L";
    }
    if (strstart(term, "vt220", NULL)) {
        s->kcub1 = "\033[D";
        s->kcud1 = "\033[B";
        s->kcuf1 = "\033[C";
        s->kcuu1 = "\033[A";
        s->kdch1 = "\033[3~";
        s->kend = "\033[4~";
        s->khome = "\033[1~";
        s->kich1 = "\033[2~";
        s->knp = "\033[6~";
        s->kpp = "\033[5~";
        s->kf1 = "\033OP";
        s->kf2 = "\033OQ";
        s->kf3 = "\033OR";
        s->kf4 = "\033OS";
        s->kf5 = "\033[17~";
        s->kf6 = "\033[18~";
        s->kf7 = "\033[19~";
        s->kf8 = "\033[20~";
        s->kf9 = "\033[21~";
        s->kf10 = "\033[29~";
    }
    if (strstart(term, "cygwin", NULL)) {
        s->kbs = "\10";
        goto linux_cygwin;
    }
    if (strstart(term, "linux", NULL)) {
        s->kbs = "\177";
        s->kb2 = "\033[G";
        s->kcbt = "\033[Z";
        s->kspd = "\032";       // ^Z
    linux_cygwin:
        s->kcub1 = "\033[D";
        s->kcud1 = "\033[B";
        s->kcuf1 = "\033[C";
        s->kcuu1 = "\033[A";
        s->kdch1 = "\033[3~";
        s->kend = "\033[4~";
        s->khome = "\033[1~";
        s->kich1 = "\033[2~";
        s->knp = "\033[6~";
        s->kpp = "\033[5~";
        s->kf1 = "\033[[A";
        s->kf2 = "\033[[B";
        s->kf3 = "\033[[C";
        s->kf4 = "\033[[D";
        s->kf5 = "\033[[E";
        s->kf6 = "\033[17~";
        s->kf7 = "\033[18~";
        s->kf8 = "\033[19~";
        s->kf9 = "\033[20~";
        s->kf10 = "\033[21~";
        s->kf11 = "\033[23~";
        s->kf12 = "\033[24~";
        s->kf13 = "\033[25~";
        s->kf14 = "\033[26~";
        s->kf15 = "\033[28~";
        s->kf16 = "\033[29~";
        s->kf17 = "\033[31~";
        s->kf18 = "\033[32~";
        s->kf19 = "\033[33~";
        s->kf20 = "\033[34~";
    }
    if (strstart(term, "xterm", NULL)) {
        s->ka1 = "\033Ow";
        s->ka3 = "\033Ou";
        s->kb2 = "\033Oy";
        s->kbeg = "\033OE";
        s->kbs = "\03377";
        s->kc1 = "\033Oq";
        s->kc3 = "\033Os";
        s->kcub1 = "\033OD";
        s->kcud1 = "\033OB";
        s->kcuf1 = "\033OC";
        s->kcuu1 = "\033OA";
        s->kdch1 = "\033[3~";
        s->kend = "\033[4~";
        s->kent = "\033OM";
        s->khome = "\033[1~";
        s->kich1 = "\033[2~";
        s->kmous = "\033[M";
        s->knp = "\033[6~";
        s->kpp = "\033[5~";
        s->kf1 = "\033OP";
        s->kf2 = "\033OQ";
        s->kf3 = "\033OR";
        s->kf4 = "\033OS";
        s->kf5 = "\033[15~";
        s->kf6 = "\033[17~";
        s->kf7 = "\033[18~";
        s->kf8 = "\033[19~";
        s->kf9 = "\033[20~";
        s->kf10 = "\033[21~";
        s->kf11 = "\033[23~";
        s->kf12 = "\033[24~";
        s->kf13 = "\033[25~";
        s->kf14 = "\033[26~";
        s->kf15 = "\033[28~";
        s->kf16 = "\033[29~";
        s->kf17 = "\033[31~";
        s->kf18 = "\033[32~";
        s->kf19 = "\033[33~";
        s->kf20 = "\033[34~";
    }
}

// XXX: should use an auxiliary buffer to make this asynchous
static void qe_term_write(ShellState *s, const char *buf, int len)
{
    int ret;

    if (len < 0)
        len = strlen(buf);

    if (s->base.qs->trace_buffer)
        qe_trace_bytes(s->base.qs, buf, len, EB_TRACE_PTY);

    while (len > 0) {
        ret = write(s->pty_fd, buf, len);
        if (ret == -1 && (errno == EAGAIN || errno == EINTR))
            continue;
        if (ret <= 0)
            break;
        buf += ret;
        len -= ret;
        s->last_char = buf[-1];
    }
}

static inline void qe_term_set_style(ShellState *s) {
    QETermStyle composite_color;

    if (s->reverse) {
        composite_color = QE_TERM_MAKE_COLOR(s->bgcolor, s->fgcolor);
    } else {
        composite_color = QE_TERM_MAKE_COLOR(s->fgcolor, s->bgcolor);
    }
    s->b->cur_style = QE_TERM_COMPOSITE | s->attr | composite_color;
}

/* return offset of the n-th terminal line from a given offset */
static int qe_term_skip_lines(ShellState *s, int offset, int n) {
    int x, y, w, offset1, offset2;
    x = y = 0;
    while (y < n && offset < s->b->total_size) {
        char32_t c = eb_nextc(s->b, offset, &offset1);
        if (c == '\n') {
            y++;
            x = 0;
        } else
        if (c == '\t') {
            w = (x + 8) & ~7;
            /* TAB at EOL does not move the cursor */
            x = min_int(x + w, s->cols - 1);
        } else {
            w = qe_wcwidth(c);
            x += w;
            if (x >= s->cols) {
                /* handle line wrapping */
                if (x > s->cols) {
                    /* wide character at EOL actually wraps to next line */
                    y++;
                    if (y == n)
                        break;
                    x = w;
                } else {
                    /* aggregate all accents */
                    while (qe_isaccent(c = eb_nextc(s->b, offset1, &offset2)))
                        offset1 = offset2;
                    if (c != '\n') {
                        /* character is at end of line, next character wraps to next line */
                        y++;
                        x = 0;
                    }
                }
            }
        }
        offset = offset1;
    }
    return offset;
}

typedef struct ShellPos {
    int screen_start; /* offset of the start of row 0 */
    int line_start; /* offset of the start of current row */
    int offset;     /* offset of the glyph */
    int line_end;   /* offset of the newline or the first character that wraps */
    int row;        /* row of the target offset */
    int col;        /* column of the target (0 based, newline may have col == s->cols) */
    int end_col;    /* column of the end of line_end */
    int flags;
#define SP_SCREEN_START_WRAP  1
#define SP_LINE_START_WRAP1   2
#define SP_LINE_START_WRAP2   4
#define SP_LINE_START_WRAP    6
#define SP_LINE_END_WRAP1     8
#define SP_LINE_END_WRAP2     16
#define SP_LINE_END_WRAP      24
} ShellPos;

#define SP_NO_UPDATE  1
static int qe_term_get_pos2(ShellState *s, int destoffset, ShellPos *spp, int flags) {
    int offset, offset0, offset1, start_offset, line_offset;
    int x, y, w, gpflags;
    char32_t c;

    if (s->use_alternate_screen) {
        start_offset = s->alternate_screen_top = min_offset(s->alternate_screen_top, s->b->total_size);
    } else {
        start_offset = s->screen_top = min_offset(s->screen_top, s->b->total_size);
    }
    if (spp) {
        gpflags = 0;
        destoffset = clamp_offset(destoffset, 0, s->b->total_size);
        offset = offset0 = line_offset = start_offset;
        for (x = y = 0; offset < destoffset;) {
            offset0 = offset;
            c = eb_nextc(s->b, offset, &offset);
            if (c == '\n') {
                y++;
                x = 0;
                gpflags &= ~SP_LINE_START_WRAP;
                line_offset = offset;
            } else
            if (c == '\t') {
                w = (x + 8) & ~7;
                /* TAB at EOL does not move the cursor */
                x = min_int(x + w, s->cols - 1);
            } else {
                w = qe_wcwidth(c);
                x += w;
                if (x >= s->cols) {
                    /* handle line wrapping */
                    if (x > s->cols) {
                        /* wide character at EOL actually wraps to next line */
                        y++;
                        x = w;
                        gpflags |= SP_LINE_START_WRAP2;
                        line_offset = offset;
                    } else {
                        /* aggregate all accents */
                        while (qe_isaccent(c = eb_nextc(s->b, offset, &offset1)))
                            offset = offset1;
                        if (c != '\n') {
                            y++;
                            x = 0;
                            gpflags |= SP_LINE_START_WRAP1;
                            line_offset = offset;
                        }
                    }
                }
            }
        }
        if (x >= s->cols - 1 && offset == destoffset) {
            /* check if current glyph causes line wrap */
            c = eb_nextc(s->b, offset, &offset1);
            if (c != '\n' && x + qe_wcwidth(c) > s->cols) {
                y++;
                x = 0;
                gpflags |= SP_LINE_START_WRAP1;
                line_offset = offset;
            }
        }
        if (y >= s->rows && !(flags & SP_NO_UPDATE)) {
            /* adjust start if row is too far */
            start_offset = qe_term_skip_lines(s, start_offset, y - s->rows + 1);
            y = s->rows - 1;
            /* update screen_top */
            if (s->use_alternate_screen)
                s->alternate_screen_top = start_offset;
            else
                s->screen_top = start_offset;
        }
        if (eb_prevc(s->b, start_offset, &offset1) != '\n')
            gpflags |= SP_SCREEN_START_WRAP;
        spp->col = x;
        spp->row = y;
        spp->line_start = line_offset;
        spp->screen_start = start_offset;
        spp->offset = offset;
        if (s->cur_offset_hack && offset == s->cur_offset) {
            c = eb_nextc(s->b, offset, &offset1);
            if (qe_iswide(c))
                spp->col += 1;
        }
        //TRACE_PRINTF(s, "qe_term_get_pos2 -> x=%d, y=%d\n", spp->col, y);
        for (;;) {
            offset0 = offset;
            c = eb_nextc(s->b, offset, &offset);
            if (c == '\n')
                break;
            if (c == '\t') {
                w = (x + 8) & ~7;
                x = min_int(x + w, s->cols - 1);
            } else {
                w = qe_wcwidth(c);
                x += w;
                if (x >= s->cols) {
                    if (x > s->cols) {
                        x -= w;
                        gpflags |= SP_LINE_END_WRAP2;
                        break;
                    }
                    /* aggregate all accents */
                    while (qe_isaccent(c = eb_nextc(s->b, offset, &offset1)))
                        offset = offset1;
                    if (c != '\n') {
                        offset0 = offset;
                        gpflags |= SP_LINE_END_WRAP1;
                        break;
                    }
                }
            }
        }
        spp->flags = gpflags;
        spp->end_col = x;
        spp->line_end = offset0;
    }
    return start_offset;
}

static int qe_term_get_pos(ShellState *s, int destoffset, int *px, int *py) {
    int offset, offset1;
    int x, y, w, start_offset;
    char32_t c;

    if (s->use_alternate_screen) {
        start_offset = s->alternate_screen_top = min_offset(s->alternate_screen_top, s->b->total_size);
    } else {
        start_offset = s->screen_top = min_offset(s->screen_top, s->b->total_size);
    }
    if (px || py) {
        destoffset = clamp_offset(destoffset, 0, s->b->total_size);
        offset = start_offset;
        for (x = y = 0; offset < destoffset;) {
            c = eb_nextc(s->b, offset, &offset);
            if (c == '\n') {
                y++;
                x = 0;
            } else
            if (c == '\t') {
                w = (x + 8) & ~7;
                /* TAB at EOL does not move the cursor */
                x = min_int(x + w, s->cols - 1);
            } else {
                w = qe_wcwidth(c);
                x += w;
                if (x >= s->cols) {
                    /* handle line wrapping */
                    if (x > s->cols) {
                        /* wide character at EOL actually wraps to next line */
                        y++;
                        x = w;
                    } else {
                        /* aggregate all accents */
                        while (qe_isaccent(c = eb_nextc(s->b, offset, &offset1)))
                            offset = offset1;
                        if (c != '\n') {
                            y++;
                            x = 0;
                        }
                    }
                }
            }
        }
        if (x >= s->cols - 1 && offset == destoffset) {
            /* check if current glyph causes line wrap */
            c = eb_nextc(s->b, offset, &offset1);
            if (c != '\n' && x + qe_wcwidth(c) > s->cols) {
                y++;
                x = 0;
            }
        }
        if (y >= s->rows) {
            // XXX: should take a flag to make this optional
            /* adjust start if row is too far */
            start_offset = qe_term_skip_lines(s, start_offset, y - s->rows + 1);
            y = s->rows - 1;
            /* update screen_top */
            if (s->use_alternate_screen)
                s->alternate_screen_top = start_offset;
            else
                s->screen_top = start_offset;
        }
        if (s->cur_offset_hack && offset == s->cur_offset) {
            c = eb_nextc(s->b, offset, &offset1);
            if (qe_iswide(c))
                x += 1;
        }
        if (px) *px = x;
        if (py) *py = y;
        //TRACE_PRINTF(s, "qe_term_get_pos -> x=%d, y=%d\n", x, y);
    }
    return start_offset;
}

/* Compute offset of the char at column x and row y (0 based).
 * Can insert spaces or newlines if needed.
 * x and y may each be relative to the current position.
 */
#define TG_RELATIVE_COL  0x01
#define TG_RELATIVE_ROW  0x02
#define TG_RELATIVE      0x03
#define TG_NOCLIP        0x04
#define TG_NOEXTEND      0x08
static int qe_term_goto_pos(ShellState *s, int offset, int destx, int desty, int flags) {
    int x, y, w, x1, y1, start_offset, offset1, offset2;
    char32_t c;

    s->cur_offset_hack = 0;

    if (flags & TG_RELATIVE) {
        start_offset = qe_term_get_pos(s, offset, &x, &y);
        if (flags & TG_RELATIVE_COL)
            destx += x;
        if (flags & TG_RELATIVE_ROW)
            desty += y;
    } else {
        start_offset = qe_term_get_pos(s, offset, NULL, NULL);
    }
    if (desty < 0 || desty >= QE_TERM_YSIZE_INFINITE - 1)
        desty = 0;
    else
    if (desty >= s->rows && !(flags & TG_NOCLIP))
        desty = s->rows - 1;
    if (destx < 0)
        destx = 0;
    else
    if (destx >= s->cols && !(flags & TG_NOCLIP))
        destx = s->cols;

    //TRACE_PRINTF(s, "goto col=%d row=%d flags=%d\n", destx, desty, flags);

    x = y = 0;
    offset = start_offset;
    while (y < desty || x < destx) {
        if (offset >= s->b->total_size) {
            // XXX: inefficient: should only test if '\n'
            offset = s->b->total_size;
            if (flags & TG_NOEXTEND)
                break;
            /* XXX: color may be wrong */
            //s->b->cur_style = QE_STYLE_DEFAULT;
            s->b->cur_style = QE_TERM_COMPOSITE | QE_TERM_MAKE_COLOR(QE_TERM_DEF_FG, QE_TERM_DEF_BG);
            //qe_term_set_style(s);
            if (y < desty) {
                // XXX: potential problem if previous line has s->cols characters
                offset += eb_insert_char32_n(s->b, offset, '\n', desty - y);
                y = desty;
                x = 0;
                /* update current screen_top */
                qe_term_get_pos(s, offset, &x1, &y1);
            }
            if (x < destx) {
                offset += eb_insert_spaces(s->b, offset, destx - x);
                x = destx;
            }
            break;
        } else {
            c = eb_nextc(s->b, offset, &offset1);
            if (c == '\n') {
                if (y < desty) {
                    y++;
                    x = 0;
                    offset = offset1;
                } else {
                    if (flags & TG_NOEXTEND)
                        break;
                    //s->b->cur_style = QE_STYLE_DEFAULT;
                    s->b->cur_style = QE_TERM_COMPOSITE | QE_TERM_MAKE_COLOR(QE_TERM_DEF_FG, QE_TERM_DEF_BG);
                    offset += eb_insert_spaces(s->b, offset, destx - x);
                    x = destx;
                }
            } else
            if (c == '\t') {
                w = (x + 8) & ~7;
                /* TAB at EOL does not move the cursor */
                x1 = min_int(x + w, s->cols - 1);
                if (y == desty && x1 > destx) {
                    /* expand TAB if destination falls in the middle */
                    if (flags & TG_NOEXTEND)
                        break;
                    eb_delete_range(s->b, offset, offset1);
                    eb_insert_spaces(s->b, offset, x1 - x);
                    continue;
                }
                x = x1;
                offset = offset1;
            } else {
                /* if destination x is in the middle of a wide character,
                   actual x will be too far */
                w = qe_wcwidth(c);
                // XXX: this test is inefficient, should first skip desty rows
                //      then skip destx columns
                if (w > 1 && y == desty && x + w > destx) {
                    /* positioning request in the middle of a wide character */
                    s->cur_offset_hack = 1;
                    break;
                }
                x += w;
                if (x >= s->cols) {
                    /* handle line wrapping */
                    if (y == desty) {
                        if (x == destx) {
                            offset = offset1;
                            break;
                        }
                        break;
                    }
                    if (x > s->cols) {
                        x = 0;
                        y++;
                        continue;
                    }
                    /* aggregate all accents */
                    while (qe_isaccent(c = eb_nextc(s->b, offset1, &offset2)))
                        offset1 = offset2;
                    if (c != '\n') {
                        x = 0;
                        y++;
                    }
                }
                offset = offset1;
            }
        }
    }
    return eb_skip_accents(s->b, offset);
}

static void qe_term_goto_xy(ShellState *s, int destx, int desty, int flags) {
    s->cur_offset = qe_term_goto_pos(s, s->cur_offset, destx, desty, flags);
}

static void qe_term_goto_tab(ShellState *s, int n) {
    /* assuming tab stops every 8 positions */
    int x, y, col_num;
    qe_term_get_pos(s, s->cur_offset, &x, &y);
    col_num = max_int(0, x + n * 8) & ~7;
    if (col_num >= s->cols) {
        /* handle wrapping lines */
        if (x < s->cols)
            col_num = s->cols - 1;
        else
            col_num = s->cols;
    }
    qe_term_goto_xy(s, col_num, y, 0);
}

/* Overwrite the current contents with an encoded glyph
 * of width w.
 * Must replace overwritten wide glyphs with spaces
 */
static int qe_term_overwrite(ShellState *s, int offset, int w,
                             const char *buf, int len)
{
    int offset1, offset2;
    int w1, x, y, x1;
    char32_t c1, c2;

    // XXX: bypass all these tests if at end of buffer?
    c1 = eb_nextc(s->b, offset, &offset1);
    if (c1 == '\n' && offset1 > offset) {
        qe_term_get_pos(s, offset, &x, &y);
        if (x + w > s->cols) {
            /* the inserted character will wrap:
             * fuse the lines and overwrite at next BOL
             */
            eb_delete_range(s->b, offset, offset1);
            c1 = eb_nextc(s->b, offset, &offset1);
        }
    }
    qe_term_set_style(s);
    if (c1 == '\n') {
        /* insert */
        eb_insert(s->b, offset, buf, len);
    } else {
        if (c1 == '\t') {
            /* expand TAB */
            qe_term_get_pos(s, offset, &x, &y);
            eb_delete_range(s->b, offset, offset1);
            w1 = (x + 8) & ~7;
            x1 = min_int(x + w1, s->cols - 1);
            if (x1 > x) {
                eb_insert_spaces(s->b, offset, x1 - x);
                c1 = eb_nextc(s->b, offset, &offset1);
            }
        }
        offset1 = eb_skip_accents(s->b, offset1);
        if (qe_iswide(c1)) {
            /* must test here before buffer modifications change s->cur_offset */
            int has_hack = s->cur_offset_hack && offset == s->cur_offset;
            eb_delete_range(s->b, offset, offset1);
            offset1 = offset;
            c2 = eb_nextc(s->b, offset, &offset2);
            if (c2 != '\n' || has_hack) {
                offset1 += eb_insert_char32(s->b, offset1, ' ');
                offset2 = offset1;
                if (c2 != '\n')
                    offset2 += eb_insert_char32(s->b, offset2, ' ');
                if (has_hack) {
                    s->cur_offset_hack = 0;
                    offset = offset1;
                    offset1 = offset2;
                }
            }
        }
        if (w > 1) {
            /* overwriting 2 positions: potentially convert a second wide glyph */
            c2 = eb_nextc(s->b, offset1, &offset2);
            if (c2 == '\n') {
                /* no adjustment needed */
            } else
            if (c2 == '\t') {
                // XXX: should expand TAB
            } else {
                offset2 = eb_skip_accents(s->b, offset2);
                if (qe_iswide(c2)) {
                    eb_delete_range(s->b, offset1, offset2);
                    if (eb_nextc(s->b, offset1, &offset2) != '\n') {
                        offset1 += eb_insert_char32(s->b, offset1, ' ');
                        eb_insert_char32(s->b, offset1, ' ');
                    }
                } else {
                    offset1 = offset2;
                }
            }
        }
        /* check for buffer content change is not an advisable optimisation
         * because re-writing the same character may cause color changes.
         */
        if (offset1 - offset == len) {
            eb_write(s->b, offset, buf, len);
        } else {
            eb_delete_range(s->b, offset, offset1);
            eb_insert(s->b, offset, buf, len);
        }
    }
    return offset + len;
}

static int qe_term_delete_lines(ShellState *s, int offset, int n)
{
    int i, offset1, offset2;

    // XXX: should scan buffer contents to handle line wrapping
    // XXX: should insert a newline if offset is inside a wrapping line
    // XXX: should update screen_top if on row 0
    if (n > 0) {
        for (offset1 = offset, i = 0; i < n; i++) {
            offset1 = eb_next_line(s->b, offset1);
        }
        if (eb_prevc(s->b, offset1, &offset2) != '\n')
            eb_prevc(s->b, offset, &offset);
        eb_delete_range(s->b, offset, offset1);
    }
    return offset;
}

static int qe_term_insert_lines(ShellState *s, int offset, int n)
{
    if (n > 0) {
        // XXX: tricky if offset is in the middle of a wrapping line
        offset += eb_insert_char32_n(s->b, offset, '\n', n);
    }
    return offset;
}

#if QE_TERM_FG_COLORS < 256
#define MAP_FG_COLOR(color)  qe_map_color(xterm_colors[color], xterm_colors, QE_TERM_FG_COLORS, NULL)
#else
#define MAP_FG_COLOR(color)  (color)
#endif
#if QE_TERM_BG_COLORS < 256
#define MAP_BG_COLOR(color)  qe_map_color(xterm_colors[color], xterm_colors, QE_TERM_BG_COLORS, NULL)
#else
#define MAP_BG_COLOR(color)  (color)
#endif

static int qe_term_csi_m(ShellState *s, const int *params, int count)
{
    int c = *params;

    /* Comment from putty/terminal.c:
     *
     * A VT100 without the AVO only had one attribute, either underline or
     * reverse video depending on the cursor type, this was selected by
     * CSI 7m.
     *
     * case 2:
     *  This is sometimes DIM, eg on the GIGI and Linux (aka FAINT in iTerm2)
     * case 8:
     *  This is sometimes INVIS various ANSI.
     * case 21:
     *  This like 22 disables BOLD, DIM and INVIS
     *
     * The ANSI colours appear on any terminal that has colour (obviously)
     * but the interaction between sgr0 and the colours varies but is usually
     * related to the background colour erase item. The interaction between
     * colour attributes and the mono ones is also very implementation
     * dependent.
     *
     * The 39 and 49 attributes are likely to be unimplemented.
     */
    switch (c) {
    case CSI_PARAM_OMITTED:
    case 0:     /* Normal (default). [exit_attribute_mode] */
        s->fgcolor = QE_TERM_DEF_FG;
        s->bgcolor = QE_TERM_DEF_BG;
        s->reverse = 0;
        s->attr = 0;
        break;
    case 1:     /* Bold. [enter_bold_mode] */
        s->attr |= QE_TERM_BOLD;
        break;
    case 2:     /* Faint, decreased intensity (ISO 6429). */
        goto unhandled;
    case 3:     /* Italicized (ISO 6429). [enter_italic_mode] */
        s->attr |= QE_TERM_ITALIC;
        break;
    case 4:     /* Underlined. [enter_underline_mode] */
        s->attr |= QE_TERM_UNDERLINE;
        break;
    case 5:     /* Blink (appears as Bold). [enter_blink_mode] */
        s->attr |= QE_TERM_BLINK;
        break;
    case 6:     /* SCO light background, ANSI.SYS blink rapid */
        goto unhandled;
    case 7:     /* Inverse. [enter_reverse_mode, enter_standout_mode] */
        s->reverse = 1;
        break;
    case 8:     /* Invisible, i.e., hidden (VT300). enter_secure_mode */
    case 9:     /* Crossed-out characters (ISO 6429). cygwin dim mode */
    case 10:    /* SCO acs off, ANSI default font */
    case 11:    /* SCO acs on (CP437) */
    case 12:    /* SCO acs on, |0x80 */
    case 13: case 14: case 15:
    case 16: case 17: case 18: case 19:  /* 11-19: nth alternate font */
    case 20:    /* Fraktur */
        goto unhandled;
    case 21:    /* Doubly-underlined (ISO 6429). Bold off sometimes */
        goto unhandled;
    case 22:    /* Normal (neither bold nor faint). [exit_bold_mode] */
        s->attr &= ~QE_TERM_BOLD;
        break;
    case 23:    /* Not italicized (ISO 6429). Not Fraktur. [exit_italic_mode] */
        s->attr &= ~QE_TERM_ITALIC;
        break;
    case 24:    /* Not underlined. [exit_underline_mode] */
        s->attr &= ~QE_TERM_UNDERLINE;
        break;
    case 25:    /* Steady (not blinking). [exit_blink_mode] */
        s->attr &= ~QE_TERM_BLINK;
        break;
    case 26:    /* reserved */
        goto unhandled;
    case 27:    /* Positive (not inverse). [exit_reverse_mode, exit_standout_mode] */
        s->reverse = 0;
        break;
    case 28:    /* Visible, i.e., not hidden (VT300). [exit_secure_mode] */
    case 29:    /* Not crossed-out (ISO 6429). */
        goto unhandled;
    case 30: case 31: case 32: case 33:
    case 34: case 35: case 36: case 37:
        /* set foreground color */
        /* 0:black 1:red 2:green 3:yellow 4:blue 5:magenta 6:cyan 7:white */
        /* XXX: should distinguish system colors and palette colors */
        // FIXME: should map the color if it has been redefined only?
        s->fgcolor = MAP_FG_COLOR(c - 30);
        break;
    case 38:    /* set extended foreground color (ISO-8613-3) */
        // First subparam means:   # additional subparams:  Accepts optional params:
        // 1: transparent          0                        NO
        // 2: RGB                  3                        YES
        // 3: CMY                  3                        YES
        // 4: CMYK                 4                        YES
        // 5: Indexed color        1                        NO
        //
        // Optional paramters go at position 7 and 8, and indicate toleranace as an
        // integer; and color space (0=CIELUV, 1=CIELAB). Example:
        //
        // CSI 38:2:255:128:64:0:5:1 m
        //
        // Also accepted for xterm compatibility, but never with optional parameters:
        // CSI 38;2;255;128;64 m
        //
        // Set the foreground color to red=255, green=128, blue=64 with a tolerance of
        // 5 in the CIELAB color space. The 0 at the 6th position has no meaning and
        // is just a filler.
        //
        // For 256-color mode (indexed) use this for the foreground:
        // CSI 38;5;N m
        // where N is a value between 0 and 255. See the colors described in screen_char_t
        // in the comments for fgColorCode.

        if (count >= 3 && params[1] == 5) {
            /* set foreground color to third esc_param */
            /* complete syntax is \033[38;5;Nm where N is in range 0..255 */
            int color = clamp_int(params[2], 0, 255);

            /* map color to qe-term palette */
            // FIXME: should map the color if it has been redefined only?
            s->fgcolor = MAP_FG_COLOR(color);
            return 3;
        }
        if (count >= 5 && params[1] == 2) {
            /* set foreground color to 24-bit color */
            /* complete syntax is \033[38;2;r;g;bm where r,g,b are in 0..255 */
            /* ??? alternate syntax: ESC [ 3 8 : 2 : Pi : Pr : Pg : Pb m */
            QEColor rgb = QERGB25(clamp_int(params[2], 0, 255),
                                  clamp_int(params[3], 0, 255),
                                  clamp_int(params[4], 0, 255));

            /* map 24-bit colors to qe-term palette */
            s->fgcolor = qe_map_color(rgb, xterm_colors, QE_TERM_FG_COLORS, NULL);
            return 5;
        }
        return 2;
    case 39:    /* orig_pair(1) [default-foreground] */
        s->fgcolor = QE_TERM_DEF_FG;
        break;
    case 40: case 41: case 42: case 43:
    case 44: case 45: case 46: case 47:
        /* set background color */
        /* XXX: should distinguish system colors and palette colors */
        // FIXME: should map the color if it has been redefined only?
        s->bgcolor = MAP_BG_COLOR(c - 40);
        break;
    case 48:    /* set extended background color (ISO-8613-3) */
        if (count >= 3 && params[1] == 5) {
            /* set background color to third esc_param */
            /* complete syntax is \033[48;5;Nm where N is in range 0..255 */
            int color = clamp_int(params[2], 0, 255);

            /* map color to qe-term palette */
            // FIXME: should map the color if it has been redefined only?
            s->bgcolor = MAP_BG_COLOR(color);
            return 3;
        }
        if (count >= 5 && params[1] == 2) {
            /* set background color to 24-bit color */
            /* complete syntax is \033[48;2;r;g;bm where r,g,b are in 0..255 */
            QEColor rgb = QERGB25(clamp_int(params[2], 0, 255),
                                  clamp_int(params[3], 0, 255),
                                  clamp_int(params[4], 0, 255));

            /* map 24-bit colors to qe-term palette */
            s->bgcolor = qe_map_color(rgb, xterm_colors, QE_TERM_BG_COLORS, NULL);
            return 5;
        }
        return 2;
    case 49:    /* orig_pair(2) [default-background] */
        s->bgcolor = QE_TERM_DEF_BG;
        break;
    case 50:    /* Reserved */
    case 51:    /* Framed */
    case 52:    /* Encircled */
    case 53:    /* Overlined */
    case 54:    /* Not framed or encircled */
    case 55:    /* Not overlined */
    case 56: case 57: case 58: case 59: /* Reserved */
    case 60:    /* ideogram underline or right side line
                 * hardly ever supported */
    case 61:    /* ideogram double underline or double line on the right side
                 * hardly ever supported */
    case 62:    /* ideogram overline or left side line
                 * hardly ever supported */
    case 63:    /* ideogram double overline or double line on the left side
                 * hardly ever supported */
    case 64:    /* ideogram stress marking
                 * hardly ever supported */
    case 65:    /* ideogram attributes off, reset the effects of all of 60-64
                 * hardly ever supported */
        goto unhandled;
    case 90: case 91: case 92: case 93:
    case 94: case 95: case 96: case 97:
        /* Set foreground text color, high intensity, aixterm (not in standard) */
        /* XXX: should distinguish system colors and palette colors */
        // FIXME: should map the color if it has been redefined only?
        s->fgcolor = MAP_FG_COLOR(c - 90 + 8);
        break;
    case 100: case 101: case 102: case 103:
    case 104: case 105: case 106: case 107:
        /* Set background color, high intensity, aixterm (not in standard) */
        /* XXX: should distinguish system colors and palette colors */
        // FIXME: should map the color if it has been redefined only?
        s->bgcolor = MAP_BG_COLOR(c - 100 + 8);
        break;
    default:
    unhandled:
        TRACE_MSG(s, "unhandled SGR");
        break;
    }
    return 1;
}


/* Well, almost a hack to update cursor */
static void qe_term_update_cursor(qe__unused__ ShellState *s)
{
#if 0
    QEmacsState *qs = s->base.qs;
    EditState *e;

    if (s->cur_offset == -1)
        return;

    for (e = qs->first_window; e != NULL; e = e->next_window) {
        if (s->b == e->b && e->interactive) {
            e->offset = s->cur_offset;
        }
    }
#endif
}

static inline ShellState *shell_get_state(EditState *e, int status)
{
    return qe_get_buffer_mode_data(e->b, &shell_mode, status ? e : NULL);
}

/* CG: much cleaner way! */
/* would need a kill hook as well ? */
static void shell_display_hook(EditState *e)
{
    ShellState *s;

    if (e->interactive) {
        if ((s = shell_get_state(e, 0)) != NULL)
            e->offset = s->cur_offset;
        if (s->use_alternate_screen)
            e->offset_top = s->alternate_screen_top;
    }
}

static void shell_key(QEmacsState *qs, void *opaque, int key)
{
    ShellState *s = opaque;
    char buf[10];
    const char *p;
    int len;

    if (!s || s->base.mode != &shell_mode)
        return;

    if (key == KEY_CTRL('o') && s->last_key_grabbed == KEY_CTRL(' ')) {
        qe_ungrab_keys(qs);
        qe_unget_key(qs, key);
        s->last_key_grabbed = key;
        return;
    }
    s->last_key_grabbed = key;
    p = buf;
    len = -1;
    switch (key) {
    case KEY_UP:        p = s->kcuu1; break;
    case KEY_DOWN:      p = s->kcud1; break;
    case KEY_RIGHT:     p = s->kcuf1; break;
    case KEY_LEFT:      p = s->kcub1; break;
    case KEY_SHIFT_TAB: p = s->kcbt;  break;
    case KEY_HOME:      p = s->khome; break;
    case KEY_INSERT:    p = s->kich1; break;
    case KEY_DELETE:    p = s->kdch1; break;
    case KEY_END:       p = s->kend;  break;
    case KEY_PAGEUP:    p = s->kpp;   break;
    case KEY_PAGEDOWN:  p = s->knp;   break;
    case KEY_F1:        p = s->kf1;   break;
    case KEY_F2:        p = s->kf2;   break;
    case KEY_F3:        p = s->kf3;   break;
    case KEY_F4:        p = s->kf4;   break;
    case KEY_F5:        p = s->kf5;   break;
    case KEY_F6:        p = s->kf6;   break;
    case KEY_F7:        p = s->kf7;   break;
    case KEY_F8:        p = s->kf8;   break;
    case KEY_F9:        p = s->kf9;   break;
    case KEY_F10:       p = s->kf10;  break;
    case KEY_F11:       p = s->kf11;  break;
    case KEY_F12:       p = s->kf12;  break;
    case KEY_F13:       p = s->kf13;  break;
    case KEY_F14:       p = s->kf14;  break;
    case KEY_F15:       p = s->kf15;  break;
    case KEY_F16:       p = s->kf16;  break;
    case KEY_F17:       p = s->kf17;  break;
    case KEY_F18:       p = s->kf18;  break;
    case KEY_F19:       p = s->kf19;  break;
    case KEY_F20:       p = s->kf20;  break;
    default:
        if (key < 256) {
            buf[0] = key;
            len = 1;
        } else
        if (key >= KEY_META(0) && key <= KEY_META(255)) {
            buf[0] = '\033';
            buf[1] = key;
            len = 2;
        } else {
            p = NULL;
        }
        break;
    }
    if (p) {
        qe_term_write(s, p, len);
    }
}

static unsigned char const sco_color[16] = {
    0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15,
};

static void qe_term_emulate(ShellState *s, int c)
{
    QEmacsState *qs = s->b->qs;
    int i, param1, param2, len, offset, offset1, offset2;
    ShellPos pos;
    char buf1[10];

    offset = s->cur_offset = clamp_offset(s->cur_offset, 0, s->b->total_size);

    if (s->state == QE_TERM_STATE_NORM) {
        if (c == 8 && s->term_pos == 1) {
            // backspace after a regular character: store in term_buf
        } else
        if (s->term_pos == 2 && s->term_buf[1] == 8 && c >= 32) {
            if (s->term_buf[0] == '_' && (c != '_' || s->term_buf[2] == '_')) {
                // special overstrike: _^HX -> underlined X
                unsigned int attr = s->attr;
                s->attr |= QE_TERM_UNDERLINE;
                s->term_buf[2] = '_';
                buf1[0] = c;
                s->cur_offset = qe_term_overwrite(s, offset, 1, buf1, 1);
                s->attr = attr;
                s->term_pos = 0;
                goto done;
            }
            if (s->term_buf[0] == c) {
                // special overstrike: X^HX -> bold X
                unsigned int attr = s->attr;
                s->attr |= QE_TERM_BOLD;
                s->term_buf[2] = 0;
                buf1[0] = c;
                s->cur_offset = qe_term_overwrite(s, offset, 1, buf1, 1);
                s->attr = attr;
                s->term_pos = 0;
                goto done;
            }
            s->term_pos = 0;
        } else {
            s->term_pos = 0;
        }
    }
    if (s->term_pos < countof(s->term_buf)) {
        /* UTF-8 bytes are appended here (among other uses of the buffer) */
        s->term_buf[s->term_pos++] = c;
        s->term_len = s->term_pos;
    }

#define ESC2(c1,c2)  (((c1) << 8) | (unsigned char)(c2))
    /* some bytes are state independent */
    switch (c) {
    case 0x18:
    case 0x1A:
        s->state = QE_TERM_STATE_NORM;
        return;
    case 0x1B:
        s->state = QE_TERM_STATE_ESC;
        return;
#if 0
    case 0x9B:  /* incompatible with UTF-8 */
        goto csi_entry;
#endif
    }

    switch (s->state) {
    case QE_TERM_STATE_NORM:
        switch (c) {
        case 5:     /* ENQ  Return Terminal Status (Ctrl-E).
             * Default response is an empty string, but may be overridden
             * by a resource <b>answerbackString</b>.
             */
            break;
        case 7:     /* BEL  Bell (Ctrl-G). */
            // XXX: should check for visible-bell
            put_status(qs->active_window, "Ding!");
            break;
        case 8:     /* BS   Backspace (Ctrl-H). */
            //TRACE_PRINTF(s, "BS: ");
            qe_term_get_pos2(s, offset, &pos, 0);
            if (pos.col == 0) {
                if (pos.row > 0 && (pos.flags & SP_LINE_START_WRAP)) {
                    //char32_t c2 =
                    eb_prev_glyph(s->b, offset, &offset);
                    //if (qe_iswide(c2))
                    //    s->cur_offset_hack = 1;
                    s->cur_offset = offset;
                }
            } else {
                /* This is iTerm2's behavior */
                pos.col -= (pos.col >= s->cols);
                qe_term_goto_xy(s, pos.col - 1, pos.row, 0);
            }
            break;
        case 9:     /* HT   Horizontal Tab (TAB) (Ctrl-I). */
            qe_term_goto_tab(s, 1);
            break;
        case 10:    /* LF   Line Feed (Ctrl-J) or New Line (NL). */
        case 11:    /* VT   Vertical Tab (Ctrl-K).
                     *      VT treated the same as LF. */
        case 12:    /* FF   Form Feed (Ctrl-L) or New Page (NP).
                     *      FF is treated the same as LF. */
            //TRACE_PRINTF(s, "LF: ");
            if (s->use_alternate_screen) {
                qe_term_goto_xy(s, 0, 1, TG_RELATIVE | TG_NOCLIP);
            } else {
                /* go to next line */
                /* CG: should check if column should be kept in cooked mode */
                if (offset >= s->b->total_size) {
                    int x, y;
                    /* add a new line */
                    /* CG: XXX: ignoring charset */
                    qe_term_set_style(s);
                    offset1 = offset + eb_insert_char32(s->b, offset, '\n');
                    s->cur_offset = offset1;
                    /* update current screen_top */
                    qe_term_get_pos(s, offset1, &x, &y);
                } else {
                    // XXX: test if cursor is on last row and append a newline
                    // XXX: potential style issue if appending a newline
                    qe_term_goto_xy(s, 0, 1, TG_RELATIVE | TG_NOCLIP);
                }
            }
            s->b->last_log = 0; /* close undo record */
            if (c == 10 && (s->shell_flags & SF_INTERACTIVE) && !s->grab_keys) {
                // Track directory from make -w -C messages and error messages
                shell_check_curpath(s, offset, c);
            }
            break;
        case 13:    /* CR   Carriage Return (Ctrl-M). */
            /* move to visual beginning of line */
            //TRACE_PRINTF(s, "CR: ");
            qe_term_goto_xy(s, 0, 0, TG_RELATIVE_ROW);
            break;
        case 14:    /* SO   Shift Out (Ctrl-N) ->
             * Switch to Alternate Character Set.  This
             * invokes the G1 character set. */
            s->shifted = s->charset[s->cset = 1];
            break;
        case 15:    /* SI   Shift In (Ctrl-O) ->
             * Switch to Standard Character Set.  This
             * invokes the G0 character set (the default). */
            s->shifted = s->charset[s->cset = 0];
            break;
        default:
            if (c >= 32) {
                /* CG: assuming ISO-8859-1 characters */
                /* CG: horrible kludge for alternate charset support */
                if (s->shifted && c >= 96 && c < 128) {
#if 0
                    /* Should actually use these tables: */
                    static const wchar_t unitab_xterm_poorman[32] =
                        "*#****o~**+++++-----++++|****L. ";
#endif
                    if (s->b->charset == &charset_utf8) {
                        static const wchar_t unitab_xterm_std[32] = {
                            0x2666, 0x2592, 0x2409, 0x240c,
                            0x240d, 0x240a, 0x00b0, 0x00b1,
                            0x2424, 0x240b, 0x2518, 0x2510,
                            0x250c, 0x2514, 0x253c, 0x23ba,
                            0x23bb, 0x2500, 0x23bc, 0x23bd,
                            0x251c, 0x2524, 0x2534, 0x252c,
                            0x2502, 0x2264, 0x2265, 0x03c0,
                            0x2260, 0x00a3, 0x00b7, 0x0020
                        };
                        s->lastc = c = unitab_xterm_std[c - 96];
                        len = utf8_encode(buf1, c);
                    } else {
                        /* CG: quick 8 bit hack: store line drawing
                         * characters in [96..127] as meta control
                         * characters in [128..159].
                         * This hack is reversed in tty_term_flush().
                         */
                        c += 32;
                        buf1[0] = s->lastc = c;
                        len = 1;
                    }
                } else {
                    /* write char (should factorize with do_char() code */
                    /* CG: Charset support is inherently broken here because
                     * bytes are inserted one at a time and charset conversion
                     * should not be performed between shell output and buffer
                     * contents. UTF8 is special cased, other charsets need work.
                     */
                    /* CG: further improvement direction includes automatic
                     * conversion from ISO-8859-1 to UTF-8 for invalid UTF-8
                     * byte sequences.
                     */
                    if (s->b->charset == &charset_utf8) {
                        s->utf8_len = utf8_length[c];
                        if (s->utf8_len > 1) {
                            s->state = QE_TERM_STATE_UTF8;
                            break;
                        }
                    }
                    //len = eb_encode_char32(s->b, buf1, c);
                    buf1[0] = s->lastc = c;
                    len = 1;
                }
                s->cur_offset = qe_term_overwrite(s, offset, 1, buf1, len);

                // Track directory from shell prompt
                if ((c == '#' || c == '$' || c == '>')
                &&  (s->shell_flags & SF_INTERACTIVE) && !s->grab_keys) {
                    shell_check_curpath(s, offset, c);
                }
            } else {
                TRACE_MSG(s, "control");
            }
            break;
        }
        break;
    case QE_TERM_STATE_UTF8:
        /* XXX: should check that c is a UTF-8 continuation byte */
        if (s->term_pos >= s->utf8_len) {
            const char *p = cs8(s->term_buf);
            char32_t ch = s->lastc = utf8_decode(&p);
            int w = qe_wcwidth(ch);
            if (w == 0) {
                /* accents are always inserted */
                // XXX: what if s->cur_offset_hack is not 0?
                // XXX: should insert a space if previous character is '\n'
                s->cur_offset += eb_insert(s->b, offset, s->term_buf, s->utf8_len);
            } else {
                s->cur_offset = qe_term_overwrite(s, offset, w, cs8(s->term_buf), s->utf8_len);
            }
            s->state = QE_TERM_STATE_NORM;
        }
        break;
    case QE_TERM_STATE_ESC:
        /* CG: should deal with other sequences:
         * ansi: hts=\EH, s0ds=\E(B, s1ds=\E)B, s2ds=\E*B, s3ds=\E+B,
         * linux: hts=\EH, rc=\E8, ri=\EM, rs1=\Ec\E]R, sc=\E7,
         * vt100: enacs=\E(B\E)0, hts=\EH, rc=\E8, ri=\EM$<5>,
         *        rmkx=\E[?1l\E>,
         *        rs2=\E>\E[?3l\E[?4l\E[?5l\E[?7h\E[?8h, sc=\E7,
         *        smkx=\E[?1h\E=,
         * xterm: enacs=\E(B\E)0, hts=\EH, is2=\E[!p\E[?3;4l\E[4l\E>,
         *        rc=\E8, ri=\EM, rmkx=\E[?1l\E>, rs1=\Ec,
         *        rs2=\E[!p\E[?3;4l\E[4l\E>, sc=\E7, smkx=\E[?1h\E=,
         *        set-window-title=\E]0;title text\007,
         * tests: \E#8  fill screen with 'E's
         * tests: \Ec   reset
         */
        s->esc1 = c;
        s->state = QE_TERM_STATE_NORM;
        switch (c) {
        case '[':   // Control Sequence Introducer (CSI is 0x9b).
            s->nb_params = 0;
            s->params[0] = CSI_PARAM_OMITTED;
            s->params[1] = CSI_PARAM_OMITTED;
            s->esc1 = 0;   /* used for both leader and intermediary bytes */
            s->state = QE_TERM_STATE_CSI;
            break;
        case ' ':
        case '#':
        case '%':
        case '(':
        case ')':
        case '*':
        case '+':
        case '-':
        case '.':
        case '/':
            s->state = QE_TERM_STATE_ESC2;
            break;
        case ']':   // Operating System Command (OSC is 0x9d).
            s->params[0] = 0;
            s->esc2 = 0;
            s->osc_len = 0;
            s->state = QE_TERM_STATE_OSC1;
            break;
        case '^':   // Privacy Message (PM  is 0x9e).
        case '_':   // Application Program Command (APC is 0x9f).
        case 'P':   // Device Control String (DCS is 0x90).
            s->params[0] = 0;
            s->esc2 = 0;
            s->state = QE_TERM_STATE_STRING;
            break;
        case '\\':  // String Terminator (ST  is 0x9c).
            TRACE_MSG(s, "stray ST");
            break;
        case '6':   // Back Index (DECBI), VT420 and up.
            break;
        case '7':   // Save Cursor (DECSC). [sc]
            qe_term_get_pos(s, offset, &s->save_x, &s->save_y);
            break;
        case '8':   // Restore Cursor (DECRC). [rc]
            qe_term_goto_xy(s, s->save_x, s->save_y, 0);
            break;
        case 'c':   // Full Reset (RIS). [rs1, reset_1string]
            s->shifted = s->charset[s->cset = 0];
            break;
        case '9':   // Forward Index (DECFI), VT420 and up.
        case '=':   // Application Keypad (DECKPAM). [smkx]
        case '>':   // Normal Keypad (DECKPNM). [rmkx, is2, rs2]
            break;
        case 'D':   // Index (IND  is 0x84).
                    // move cursor down, scroll if at bottom
        case 'E':   // Next Line (NEL  is 0x85).
                    // move cursor to beginning of next line, scroll if at bottom
            {
                int col, row;
                qe_term_get_pos(s, offset, &col, &row);
                if (c == 'E')
                    col = 0;
                qe_term_goto_xy(s, col, row + 1, TG_NOCLIP);
            }
            break;
        case 'M':   // Reverse Index (RI  is 0x8d). [ri]
                    // move cursor up, scroll if at top line
            {
                int start, offset3, col, row;
                start = qe_term_get_pos(s, offset, &col, &row);
                if (--row < 0) {
                    /* if (start == 0) */ {
                        qe_term_insert_lines(s, start, 1);
                    }
                    offset3 = qe_term_skip_lines(s, start, s->rows - 1);
                    qe_term_delete_lines(s, offset3, 1);
                    row = 0;
                }
                qe_term_goto_xy(s, col, row, 0);
            }
            break;
        case 'F':   // Cursor to lower left corner of screen.
                    // This is enabled by the hpLowerleftBugCompat resource.
        case 'H':   // Tab Set (HTS  is 0x88). [set_tab]
        case 'O':   // Single Shift Select of G3 Character Set (SS3  is 0x8f).
                    // This affects next character only.
        case 'V':   // Start of Guarded Area (SPA  is 0x96).
        case 'W':   // End of Guarded Area (EPA  is 0x97).
        case 'X':   // Start of String (SOS  is 0x98).
        case 'Z':   // Return Terminal ID (DECID is 0x9a).
                    // Obsolete form of CSI c  (DA).
        case 'l':   // Memory Lock (per HP terminals).  Locks memory above the cursor.
        case 'm':   // Memory Unlock (per HP terminals).
        case 'n':   // Invoke the G2 Character Set as GL (LS2).
        case 'o':   // Invoke the G3 Character Set as GL (LS3).
        case '|':   // Invoke the G3 Character Set as GR (LS3R).
        case '}':   // Invoke the G2 Character Set as GR (LS2R).
        case '~':   // Invoke the G1 Character Set as GR (LS1R).
        default:
            TRACE_MSG(s, "unhandled");
            break;
        }
        break;
    case QE_TERM_STATE_ESC2:
        s->state = QE_TERM_STATE_NORM;
        s->esc2 = c;
        switch (ESC2(s->esc1, c)) {
        case ESC2('%','8'):     /* set utf mode */
        case ESC2('%','G'):     /* Select UTF-8 character set (ISO 2022). */
        case ESC2('%','@'):     /* Select default character set.
                                   That is ISO 8859-1 (ISO 2022). */
            TRACE_MSG(s, "utf mode");
            break;
            /* Designate G0 Character Set (ISO 2022, VT100). */
        case ESC2('(','0'):     /* set charset0 CSET_LINEDRW */
            s->charset[0] = 1;
            break;
        case ESC2('(','A'):     /* set charset0 CSET_GBCHR */
        case ESC2('(','B'):     /* set charset0 CSET_ASCII */
        case ESC2('(','U'):     /* set charset0 CSET_SCOACS */
            s->charset[0] = 0;
            break;
            /* Designate G1 Character Set (ISO 2022, VT100). */
        case ESC2(')','0'):     /* set charset1 CSET_LINEDRW */
            s->charset[1] = 1;
            break;
        case ESC2(')','A'):     /* set charset1 CSET_GBCHR */
        case ESC2(')','B'):     /* set charset1 CSET_ASCII */
        case ESC2(')','U'):     /* set charset1 CSET_SCOACS */
            s->charset[1] = 0;
            break;
            /* Designate G2 Character Set (ISO 2022, VT220). */
        case ESC2('*','B'):
            /* Designate G3 Character Set (ISO 2022, VT220). */
        case ESC2('+','B'):
            /* Designate G1 Character Set (VT300). */
        case ESC2('-','B'):
            /* Designate G2 Character Set (VT300). */
        case ESC2('.','B'):
            /* Designate G3 Character Set (VT300). */
        case ESC2('/','B'):
            TRACE_MSG(s, "set charset");
            break;
        default:
            TRACE_MSG(s, "unhandled");
            break;
        }
        s->shifted = s->charset[s->cset];
        break;
    case QE_TERM_STATE_OSC1:
            /* XXX: OSC sequences should use QE_TERM_STATE_STRING
               (eg: OSC Ps;Pt ST) but linux tty uses non standard
               sequences ESC ] R and `ESC ] P prrggbb, so we need
               these extra states to avoid hanging the terminal
             */
        if (s->term_pos == 3) {
            s->esc2 = c;
            if (c == 'R') {      /* linux reset palette */
                s->state = QE_TERM_STATE_NORM;
                break;
            }
        }
        if (s->esc2 == 'P') {
            if (s->term_pos < 10)
                break;
            /* linux set palette syntax is OSC P nrrggbb:
               n: letter 0-f for standard palette entries
                         g-m for extended attributes:
               rr, gg, bb 2 hex digit values
             */
            TRACE_MSG(s, "linux palette");
            s->state = QE_TERM_STATE_NORM;
            break;
        }
        if (c >= '0' && c <= '9') {
            s->params[0] *= 10;
            s->params[0] += c - '0';
            break;
        }
        s->state = QE_TERM_STATE_STRING;
        /* fall thru */
    case QE_TERM_STATE_STRING:
        /* Stop string on CR or LF, for protection */
        if ((c == '\012' || c == '\015') && s->params[0] != 1337) {
            s->state = QE_TERM_STATE_NORM;
            TRACE_MSG(s, "broken string");
            break;
        }
        /* Stop string on \a (^G) or ST (ESC \) */
        if (!(c == '\007' || c == 0x9c || (s->lastc == 27 && c == '\\'))) {
            s->lastc = c;
            /* skip ';' following OSC number */
            if (s->osc_len > 0 || (c != ';')) {
                if (s->osc_len < countof(s->osc_buf) - 1)
                    s->osc_buf[s->osc_len++] = c;
            }
            break;
        }

        /* if stopped on (ESC \) strip trailing ESC */
        if (s->osc_len > 0 && s->osc_buf[s->osc_len - 1] == 27)
            s->osc_len -= 1;

        s->osc_buf[s->osc_len] = '\0';
        s->state = QE_TERM_STATE_NORM;
        /* OSC / PM / APC / DCS string has been received. Should handle these cases:
           - OSC 0; Pt ST  Change Icon Name and Window Title to Pt.
           - OSC 1; Pt ST  Change Icon Name to Pt.
           - OSC 2; Pt ST  Change Window Title to Pt.
           - OSC 3; Pt ST  Set X property on top-level window: Pt should
           be in the form "prop=value", or just "prop" to delete the property
           - OSC 4; c; name... ST  xterm's define extended color (executeXtermSetRgb)
           Change Color #c to name. Any number of c / name pairs may be given.
           example: "\033]4;16;rgb:0000/00000/0000\033\134"

           iTerm2 reports the current rgb value with "<index>;?",
           e.g. "\033]4;105;?" -> report as \033]4;105;rgb:0000/cccc/ffff\007"

           iTerm2 has a specific behavior for colors 16 to 22:
                16: terminalSetForegroundColor
                17: terminalSetBackgroundColor
                18: terminalSetBoldColor
                19: terminalSetSelectionColor
                20: terminalSetSelectedTextColor
                21: terminalSetCursorColor
                22: terminalSetCursorTextColor

           xterm has the following set extended attribute:
           - OSC 10; c; name... ST    Change color names starting with
           text foreground (a list of one or more color names or RGB
           specifications, separated by semicolon, up to eight, name as
           as per XParseColor.

           - OSC 11; c; name... ST  Change colors starting with text background
           - OSC 12; c; name... ST  Change colors starting with text cursor
           - OSC 13; c; name... ST  Change colors starting with mouse foreground
           - OSC 14; c; name... ST  Change colors starting with mouse background
           - OSC 15; c; name... ST  Change colors starting with Tek foreground
           - OSC 16; c; name... ST  Change colors starting with Tek background
           - OSC 17; c; name... ST  Change colors starting with highlight
           - OSC 22; Pt ST Change pointer cursor shape to Pt.  The parameter
             Pt sets the pointerShape resource.  If Pt is empty, or does not
             match any of the standard names, xterm uses the resource's
             default "xterm" shape.
           - OSC 46; Pt ST  Change Log File to Pt (normally disabled by a
           compile-time option)
           - OSC 50; Pt ST  Set Font to Pt. If Pt begins with a "#", index in
           the font menu, relative (if the next character is a plus or minus
           sign) or absolute. A number is expected but not required after the
           sign (the default is the current entry for relative, zero for
           absolute indexing).
           - OSC W; Pt ST   word-set (define char wordness)
           xterm has a file download and image display protocol:
           - OSC 1337; Pt ST
         */
        switch (s->params[0]) {
        case 7:
            {
                char cwd[MAX_FILENAME_SIZE];
                buf_t cwdbuf[1];
                const char *p;

                if (!strstart(s->osc_buf, "file://", &p)) {
                    put_error(qs->active_window,
                              "term_emulate: OSC 7 URI must start with 'file://'");
                    break;
                }

                /* skip hostname if any */
                while (*p && *p != '/')
                    p++;

                if (!*p) {
                    put_error(qs->active_window,
                              "term_emulate: missing path in OSC 7 URI: '%s'", s->osc_buf);
                    break;
                }
                /* decode URI path */
                buf_init(cwdbuf, cwd, sizeof(cwd));
                while (*p) {
                    if (*p == '%' && qe_isxdigit(p[1]) && qe_isxdigit(p[2])) {
                        buf_put_byte(cwdbuf, qe_digit_value(p[1]) * 16 + qe_digit_value(p[2]));
                        p += 3;
                    } else {
                        buf_put_byte(cwdbuf, *p);
                        p += 1;
                    }
                }
                append_slash(cwd, countof(cwd));
                shell_add_cwd(s->b, s->b->offset, cwd, 0);
            }
            break;
        case 10:
        case 11:
            {
                char *p = s->osc_buf;
                QEColor color;
                int n;
                for (n = s->params[0]; n <= 11; n++) {
                    char *p1 = strchr(p, ';');
                    if (p1)
                        *p1 = '\0';
                    if (!*p || css_get_color(&color, p))
                        break;
                    if (n == 10)
                        qe_styles[QE_STYLE_SHELL].fg_color = color;
                    else
                        qe_styles[QE_STYLE_SHELL].bg_color = color;
                    if (!p1)
                        break;
                    p = p1 + 1;
                    p += (*p == ';');
                }
                break;
            }
        default:
            TRACE_PRINTF(s, "unhandled string: %.*s", min_int(s->osc_len, 32), s->osc_buf);
            break;
        }
        break;
    case QE_TERM_STATE_CSI:
        if (c >= '<' && c <= '?') {
            /* leader bytes */
            s->esc1 = c;
            break;
        }
        if (c >= 0x20 && c <= 0x2F) {
            /* intermediary bytes */
            s->esc1 = c;    /* no need to distinguish leader and interm bytes */
            break;
        }
        if (qe_isdigit(c)) {
            s->params[s->nb_params] &= ~CSI_PARAM_OMITTED;
            s->params[s->nb_params] *= 10;
            s->params[s->nb_params] += c - '0';
            break;
        }
        /* XXX: should clarify */
        if (s->nb_params == 0
        ||  (s->nb_params < MAX_CSI_PARAMS && s->params[s->nb_params] >= 0)) {
            s->nb_params++;
            s->params[s->nb_params] = CSI_PARAM_OMITTED;
        }
        if (c == ';' || c == ':')
            break;
        s->state = QE_TERM_STATE_NORM;
        /* default param is 1 for most commands */
        param1 = s->params[0] >= 0 ? s->params[0] : 1;
        param2 = s->params[1] >= 0 ? s->params[1] : 1;
        // FIXME: should handle c < 0x20 and c >= 0x7F according to ECMA-48 */
        switch (ESC2(s->esc1,c)) {
        case '@':  /* ICH: Insert Ps (Blank) Character(s) (default = 1) */
            {
                char32_t c2;
                int x, y, x1, y1, offset3;

                // XXX: should simplify this mess
                offset1 = offset;
                while (param1-- > 0) {
                    qe_term_get_pos(s, offset1, &x, &y);
                    if (x >= s->cols)
                        break;
                    offset2 = qe_term_goto_pos(s, offset1, s->cols, y, TG_NOCLIP | TG_NOEXTEND);
                    if (offset2 > offset) {
                        qe_term_get_pos(s, offset2, &x1, &y1);
                        if (y1 > y || x1 >= s->cols) {
                            /* full line: must delete last glyph (including accents) */
                            c2 = eb_prev_glyph(s->b, offset2, &offset3);
                            eb_delete_range(s->b, offset3, offset2);
                            /* if last glyph was wide, pad with a space */
                            if (qe_iswide(c2) && eb_nextc(s->b, offset3, &offset2) != '\n')
                                eb_insert_char32(s->b, offset3, ' ');
                        }
                    }
                    qe_term_set_style(s);
                    offset1 += eb_insert_char32(s->b, offset1, ' ');
                }
                /* cur_offset may have been updated by callback */
                s->cur_offset = offset;
            }
            break;
        case 'A':  /* CUU: Cursor Up Ps Times (default = 1) */
            qe_term_goto_xy(s, 0, -param1, TG_RELATIVE);
            break;
        case 'B':  /* CUD: Cursor Down Ps Times (default = 1) */
        case 'e':  /* VPR: Line Position Relative [rows] (default = 1) */
            qe_term_goto_xy(s, 0, param1, TG_RELATIVE);
            break;
        case 'C':  /* CUF: Cursor Forward Ps Times (default = 1) */
        case 'a':  /* HPR: Character Position Relative [columns] (default = 1) */
            qe_term_goto_xy(s, param1, 0, TG_RELATIVE);
            break;
        case 'D':  /* CUB: Cursor Backward Ps Times (default = 1) */
            qe_term_goto_xy(s, -param1, 0, TG_RELATIVE);
            break;
        case 'E':  /* CNL: Cursor Next Line Ps Times (default = 1) and CR. */
            qe_term_goto_xy(s, 0, param1, TG_RELATIVE_ROW);
            break;
        case 'F':  /* CPL: Cursor Preceding Line Ps Times (default = 1) and CR. */
            qe_term_goto_xy(s, 0, -param1, TG_RELATIVE_ROW);
            break;
        case 'G':  /* CHA: Cursor Character Absolute [column]. */
        case '`':  /* HPA: Character Position Absolute [column] (default = 1) */
            qe_term_goto_xy(s, param1 - 1, 0, TG_RELATIVE_ROW);
            break;
        case 'H':  /* CUP: Cursor Position [row;column] (default = [1,1]). */
        case 'f':  /* HVP: Horizontal and Vertical Position [row;column] (default = [1,1]) */
            qe_term_goto_xy(s, param2 - 1, param1 - 1, 0);
            break;
        case 'I':  /* CHT: Cursor Forward Tabulation Ps tab stops (default = 1). */
            qe_term_goto_tab(s, param1);
            break;
        case 'J':  /* ED: Erase in Display. */
        case ESC2('?','J'):  /* DECSED: Selective Erase in Display. */
            /* XXX: should just force top of window to in infinite scroll mode */
            {   /*     0: Below (default), 1: Above, 2: All, 3: Saved Lines (xterm) */
                /* XXX: should handle eol style */
                int offset0, bos, eos, col, row;

                bos = eos = 0;
                // default param is 0
                if (s->params[0] <= 0) {
                    /* Erase to end of screen */
                    eos = 1;
                } else
                if (s->params[0] == 1) {
                    /* Erase from beginning of screen */
                    bos = 1;
                } else
                if (s->params[0] == 2 || s->params[0] == 3) {
                    /* Erase complete screen */
                    bos = eos = 1;
                }
                /* update cursor as overwriting characters may change offsets */
                if (bos) {
                    offset0 = qe_term_get_pos(s, offset, &col, &row);
                    qe_term_set_style(s);
                    if (row > 0) {
                        offset = qe_term_delete_lines(s, offset0, row);
                        offset = qe_term_insert_lines(s, offset, row);
                    } else {
                        /* goto to beginning of line */
                        offset = qe_term_goto_pos(s, offset, 0, 0, TG_RELATIVE_ROW);
                    }
                    //offset = qe_term_erase_chars(s, offset, col);
                    offset1 = qe_term_goto_pos(s, offset, col, 0, TG_RELATIVE_ROW | TG_NOEXTEND);
                    eb_delete(s->b, offset, offset1 - offset);
                    offset += eb_insert_spaces(s->b, offset, col);
                }
                if (eos) {
                    eb_delete(s->b, offset, s->b->total_size - offset);
                }
                s->cur_offset = offset;
            }
            break;
        case 'K':  /* EL: Erase in Line. */
        case ESC2('?','K'):  /* DECSEL: Selective Erase in Line. */
            {   /*     0: to Right (default), 1: to Left, 2: All */
                /* XXX: should handle eol style */
                int col, row, col2, row2, n1, n2, offset3;

                // XXX: should use qe_term_get_pos2()
                qe_term_get_pos(s, offset, &col, &row);
                offset1 = qe_term_goto_pos(s, offset, 0, row, TG_NOCLIP | TG_NOEXTEND);
                offset2 = qe_term_goto_pos(s, offset, s->cols, row, TG_NOCLIP | TG_NOEXTEND);
                qe_term_get_pos(s, offset2, &col2, &row2);
                if (row2 > row)
                    col2 = s->cols;

                n1 = n2 = 0;
                // default param is 0
                if (s->params[0] <= 0) {
                    n2 = offset2 - offset;
                } else
                if (s->params[0] == 1) {
                    n1 = col;
                } else
                if (s->params[0] == 2) {
                    n1 = col;
                    n2 = offset2 - offset;
                }
                qe_term_set_style(s);
                if (n2) {
                    if (eb_nextc(s->b, offset2, &offset3) == '\n') {
                        if (col == 0 && eb_prevc(s->b, offset1, &offset3) != '\n') {
                            /* keep a space to prevent line fusion */
                            eb_insert_spaces(s->b, offset2, 1);
                        }
                    } else {
                        eb_insert_spaces(s->b, offset2, col2 - col);
                    }
                    eb_delete(s->b, offset, n2);
                }
                if (n1) {
                    /* update offset as overwriting characters may change offsets */
                    //offset = qe_term_erase_chars(s, offset1, n1);
                    offset -= eb_delete(s->b, offset1, n1);
                    offset += eb_insert_spaces(s->b, offset1, col);
                }
                // XXX: could scan end of buffer for spaces with default style
                //      and shrink it.
                /* update cursor as callback may have changed it */
                s->cur_offset = offset;
            }
            break;
        case 'L':  /* IL: Insert Ps Line(s) (default = 1). */
            //TRACE_MSG(s, "insert lines");
            {
                int row, zone;
                // XXX: should use qe_term_get_pos() and qe_term_goto_xy()
                offset = eb_goto_bol(s->b, offset);
                qe_term_get_pos(s, offset, NULL, &row);
                zone = max_int(0, s->scroll_bottom - row);
                param1 = min_int(param1, zone);
                qe_term_set_style(s);
                offset1 = qe_term_insert_lines(s, offset, param1);
                offset1 = qe_term_skip_lines(s, offset1, zone - param1);
                qe_term_delete_lines(s, offset1, param1);
                s->cur_offset = offset;
            }
            break;
        case 'M':  /* DL: Delete Ps Line(s) (default = 1). */
            //TRACE_MSG(s, "delete lines");
            {
                int row, zone;
                // XXX: should use qe_term_get_pos() and qe_term_goto_xy()
                offset = eb_goto_bol(s->b, offset);
                qe_term_get_pos(s, offset, NULL, &row);
                zone = max_int(0, s->scroll_bottom - row);
                param1 = min_int(param1, zone);
                qe_term_set_style(s);
                offset1 = qe_term_delete_lines(s, offset, param1);
                offset1 = qe_term_skip_lines(s, offset1, zone - param1);
                qe_term_insert_lines(s, offset1, param1);
                s->cur_offset = offset;
            }
            break;
        case 'P':  /* DCH: Delete Ps Character(s) (default = 1). */
            qe_term_get_pos2(s, offset, &pos, 0);
            qe_term_set_style(s);
            if (!(pos.flags & SP_LINE_END_WRAP)) {
                /* no need to pad end of line */
                if (param1 >= pos.end_col - pos.col) {
                    eb_delete_range(s->b, offset, pos.line_end);
                    if (pos.col == 0 && (pos.flags & SP_LINE_START_WRAP)) {
                        /* if removed wrapped wide glyph, pad previous line */
                        if (pos.flags & SP_LINE_START_WRAP2)
                            s->cur_offset = offset += eb_insert_char32(s->b, offset, ' ');
                        /* we removed the wrapped glyph, must insert a space */
                        eb_insert_char32(s->b, offset, ' ');
                    }
                } else {
                    offset1 = qe_term_goto_pos(s, offset, param1, 0, TG_RELATIVE | TG_NOEXTEND);
                    eb_delete_range(s->b, offset, offset1);
                    /* if removed wrapped wide glyph, pad previous line */
                    if (pos.col == 0 && (pos.flags & SP_LINE_START_WRAP2)) {
                        s->cur_offset = offset += eb_insert_char32(s->b, offset, ' ');
                    }
                }
            } else {
                if (param1 >= pos.end_col - pos.col) {
                    eb_delete_range(s->b, offset, pos.line_end);
                    eb_insert_spaces(s->b, offset, s->cols - pos.col);
                } else {
                    // XXX: should untabify rest of line
                    offset1 = qe_term_goto_pos(s, offset, param1, 0, TG_RELATIVE | TG_NOEXTEND);
                    pos.line_end -= eb_delete_range(s->b, offset, offset1);
                    eb_insert_spaces(s->b, pos.line_end, param1);
                }
            }
            break;
        case 'S':  /* SU: Scroll up Ps lines (default = 1). */
            /* scroll the whole page up */
            TRACE_MSG(s, "scroll up");
            break;
        case 'T':  /* SD: Scroll down Ps lines (default = 1). */
            /* scroll the whole page down */
            TRACE_MSG(s, "scroll down");
            break;
        case 'X':  /* ECH: Erase Ps Character(s) (default = 1). */
            // XXX: this clipping is vain as current col may be > 0.
            //      should clip better
            param1 = min_int(param1, s->cols);
            len = eb_encode_char32(s->b, buf1, ' ');
            while (param1 --> 0) {
                offset = qe_term_overwrite(s, offset, 1, buf1, len);
            }
            break;
        case 'Z':  /* CBT: Cursor Backward Tabulation Ps tab stops (default = 1). */
            qe_term_goto_tab(s, -param1);
            break;
        case 'b':  /* REP: Repeat the preceding graphic character Ps times. */
            {
                int rep = min_int(param1, s->cols);
                int w = qe_wcwidth(s->lastc);
                len = eb_encode_char32(s->b, buf1, s->lastc);
                while (rep --> 0) {
                    s->cur_offset = qe_term_overwrite(s, s->cur_offset, w, buf1, len);
                }
            }
            break;
        case 'c':  /* DA: Send Device Attributes (Primary DA) */
            // default param is 0
            if (s->params[0] <= 0) {
                /* Report Advanced Video option (AVO) */
                qe_term_write(s, "\033[?1;2c", -1);
            }
            break;
        case ESC2('>','c'):  /* DA: Send Device Attributes (Secondary DA) */
            // default param is 0
            if (s->params[0] <= 0) {
                /* Report Qemacs emulator version 0.5 */
                qe_term_write(s, "\033[>42;0;5c", -1);
            }
            break;
        case 'd':  /* VPA: Line Position Absolute [row] (default = 1). */
            param1 = min_int(param1, s->rows);
            qe_term_goto_xy(s, 0, param1 - 1, TG_RELATIVE_COL);
            break;
        case 'g':  /* TBC: Tab Clear. */
            // 0 (default) -> Clear Current Column, 3 -> Clear All */
            TRACE_MSG(s, "clear tabs");
            break;
        case 'h':   /* SM: Set Mode. */
            for (i = 0; i < s->nb_params; i++) {
                switch (s->params[i]) {
                case 2:   /* Keyboard Action Mode (AM). */
                case 4:   /* Insert Mode (IRM). */
                case 12:  /* Send/receive (SRM). */
                case 20:  /* Automatic Newline (LNM). */
                    break;
                }
            }
            break;
        case ESC2('?','h'): /* DEC Private Mode Set (DECSET) */
            for (i = 0; i < s->nb_params; i++) {
                switch (s->params[i]) {
                case 1:     /* Application Cursor Keys (DECCKM). */
                case 3:     /* Set Number of Columns to 132 (DECCOLM) */
                case 4:     /* Smooth (Slow) Scroll (DECSCLM). */
                    break;
                case 5:     /* Reverse Video (DECSCNM) */
                    s->reverse = 1;
                    break;
                case 7:     /* Wraparound Mode (DECAWM). */
                    break;
                case 12:    /* Start Blinking Cursor (att610). */
                    break;
                case 25:    /* Show Cursor (DECTCEM). */
                    break;
                case 1000:  /* Send Mouse X & Y on button press and
                                release. */
                    break;
                case 1034:  /* Interpret "meta" key, sets eighth bit.
                     * (enables the eightBitInput resource).
                     */
                    break;
                case 1047:  /* Use Alternate Screen Buffer. */
                case 1048:  /* Save cursor as in DECSC. */
                case 1049:  /* Save cursor as in DECSC and use Alternate
                       Screen Buffer, clearing it first. */
                    if (s->shell_flags & SF_INTERACTIVE) {
                        /* only grab keys in interactive qe_term buffers */
                        s->grab_keys = 1;
                        qe_grab_keys(s->base.qs, shell_key, s);
                        /* Should also clear screen */
                    }
                    if (!s->use_alternate_screen) {
                        offset = s->b->total_size;
                        if (eb_prevc(s->b, offset, &offset1) != '\n') {
                            qe_term_set_style(s);
                            offset += eb_insert_char32(s->b, offset, '\n');
                        }
                        s->use_alternate_screen = 1;
                        s->cur_offset = s->alternate_screen_top = offset;
                        // XXX: should update window top?
                    }
                    break;
                default:
                    TRACE_MSG(s, "mode set");
                    break;
                }
            }
            break;
        case 'i':   /* MC: Media Copy */
            for (i = 0; i < s->nb_params; i++) {
                switch (s->params[i]) {
                case 0:   /* Print screen (default). */
                case 4:   /* Turn off printer controller mode. */
                case 5:   /* Turn on printer controller mode. */
                case 10:  /* HTML screen dump. */
                case 11:  /* SVG screen dump. */
                    break;
                }
            }
            TRACE_MSG(s, "media copy");
            break;
        case ESC2('?','i'):  /* MC: Media Copy (MC, DEC-specific). */
            for (i = 0; i < s->nb_params; i++) {
                switch (s->params[i]) {
                case 1:   /* Print line containing cursor. */
                case 4:   /* Turn off autoprint mode. */
                case 5:   /* Turn on autoprint mode. */
                case 10:  /* Print composed display, ignores DECPEX. */
                case 11:  /* Print all pages. */
                    break;
                }
            }
            TRACE_MSG(s, "media copy");
            break;
        case 'l':   /* RM: Reset Mode. */
            for (i = 0; i < s->nb_params; i++) {
                switch (s->params[i]) {
                case 2:   /* Keyboard Action Mode (AM). */
                case 4:   /* Insert Mode (IRM). */
                case 12:  /* Send/receive (SRM). */
                case 20:  /* Automatic Newline (LNM). */
                    break;
                }
            }
            break;
        case ESC2('?','l'): /* DEC Private Mode Reset (DECRST). */
            for (i = 0; i < s->nb_params; i++) {
                switch (s->params[i]) {
                case 1:     /* Normal Cursor Keys (DECCKM). */
                case 3:     /* Set Number of Columns to 80 (DECCOLM) */
                case 4:     /* Jump (Fast) Scroll (DECSCLM). */
                    break;
                case 5:     /* Normal Video (DECSCNM). */
                    s->reverse = 1;
                    break;
                case 7:     /* No Wraparound Mode (DECAWM). */
                    break;
                case 12:    /* Stop Blinking Cursor (att610). */
                    break;
                case 25:    /* Hide Cursor (DECTCEM). */
                    break;
                case 1000:  /* Don't send Mouse X & Y on button press and
                                release. */
                case 1034:  /* Don't interpret "meta" key.  (This disables
                       the eightBitInput resource).
                     */
                    break;
                case 1047:  /* Use Normal Screen Buffer, clearing screen
                       first if in the Alternate Screen. */
                case 1048:  /* Restore cursor as in DECRC. */
                case 1049:  /* Use Normal Screen Buffer and restore cursor
                       as in DECRC. */
                    if (s->shell_flags & SF_INTERACTIVE) {
                        qe_ungrab_keys(s->base.qs);
                        s->grab_keys = 0;
                    }
                    if (s->use_alternate_screen) {
                        // XXX: this will actually go to row s->rows-1
                        qe_term_goto_xy(s, 0, s->rows, 0);
                        eb_delete_range(s->b, s->cur_offset, s->b->total_size);
                        s->use_alternate_screen = 0;
                    }
                    s->cur_offset = s->b->total_size;
                    break;
                default:
                    TRACE_MSG(s, "mode reset");
                    break;
                }
            }
            break;
        case 'm':  /* SGR: Set Graphics Rendition (Character Attributes). */
            for (i = 0; i < s->nb_params;) {
                i += qe_term_csi_m(s, s->params + i, s->nb_params - i);
            }
            break;
        case 'n':  /* DSR: Device Status Report. */
            if (param1 == 5) {
                /* Status Report: terminal is OK */
                qe_term_write(s, "\033[0n", -1);
            } else
            if (param1 == 6) {
                /* Report Cursor Position (CPR) [row;column]. */
                char buf2[20];
                int col_num, cur_line, x, y;
                /* actually send position of point in window */
                qe_term_get_pos(s, offset, &x, &y);
                col_num = x + (x < s->cols);
                cur_line = y + (y < s->rows);
                snprintf(buf2, sizeof(buf2), "\033[%d;%dR", cur_line, col_num);
                qe_term_write(s, buf2, -1);
            }
            break;
        case 'p':  /* DECSCL: Set conformance level. Also redefine keyboard keys */
        case 'q':  /* DECLL: Load LEDs. */
        case ESC2(' ','q'): /* Set cursor style (DECSCUSR, VT520). */
            goto unhandled;
        case 'r':  /* DECSTBM: Set Scrolling Region [top;bottom]
                      (default = full size of window) */
            /* XXX: the scrolling region should also affect LF operation */
            s->scroll_top = clamp_int(s->params[0] - 1, 0, s->rows);
            s->scroll_bottom = s->params[1] > 0 ? clamp_int(s->params[1], 1, s->rows) : s->rows;
            break;
        case ESC2('$','r'): /* DECCARA: Change Attributes in Rectangular
                               Area, VT400 and up. */
            goto unhandled;
        case ESC2('?','r'):
            /* Restore DEC Private Mode Values.  The value of Ps previously
               saved is restored.  Ps values are the same as for DECSET. */
            TRACE_MSG(s, "mode restore");
            break;
        case ESC2('?','s'):
            /* Save DEC Private Mode Values.  <i>Ps</i> values are the same as for
               DECSET.. */
            TRACE_MSG(s, "mode save");
            break;
        case 's':  /* Save cursor (ANSI.SYS), available only when
                      DECLRMM is disabled. */
            /* DECSLRM: Set left and right margins, available only when
               DECLRMM is enabled (VT420 and up). */
            qe_term_get_pos(s, offset, &s->save_x, &s->save_y);
            break;
        case 't':  /* DECSLPP: set page size - ie window height */
            /* also used for window changing and reports */
            TRACE_MSG(s, "set page size");
            break;
        case ESC2('>','t'):  /* Set one or more features of the title modes. */
            // Ps = 0  -> Set window/icon labels using hexadecimal.
            // Ps = 1  -> Query window/icon labels using hexadecimal.
            // Ps = 2  -> Set window/icon labels using UTF-8.
            // Ps = 3  -> Query window/icon labels using UTF-8.  (See discussion of "Title Modes")
            break;
        case ESC2('$','t'):  /* DECRARA: Reverse Attributes in Rectangular Area,
               VT400 and up. */
            goto unhandled;
        case 'u':  /* Restore cursor (ANSI.SYS). */
            qe_term_goto_xy(s, s->save_x, s->save_y, 0);
            break;
        case ESC2('$','v'):  /* DECCRA: Copy Rectangular Area (VT400 and up). */
            goto unhandled;
        case 'x':  /* DECREQTPARM: Request Terminal Parameters */
        case ESC2('$','x'):  /* DECFRA: Fill Rectangular Area, VT420 and up. */
        case ESC2('$','z'):  /* DECERA: Erase Rectangular Area, VT400 and up. */
        case ESC2('$','{'): /* DECSERA: Selective Erase Rectangular Area, VT400 and up. */
        case ESC2('\'','{'): /* DECSLE: Select Locator Events. */
        case ESC2('\'','|'): /* DECRQLP: Request Locator Position. */
        case ESC2('\'','}'): /* DECIC: Insert Ps Column(s) (default = 1), VT420 and up. */
        case ESC2('\'','~'): /* DECDC: Delete Ps Column(s) (default = 1), VT420 and up. */
        case ESC2('=','c'):   /* Hide or Show Cursor */
                              /* 0: hide, 1: restore, 2: block */
        case ESC2('=','C'):  /* set cursor shape */
        case ESC2('=','D'):  /* set blinking attr on/off */
        case ESC2('=','E'):  /* set blinking on/off */
            goto unhandled;
        case ESC2('=','F'): /* select SCO foreground color */
            s->fgcolor = sco_color[param1 & 15];
            break;
        case ESC2('=','G'): /* select SCO background color */
            s->bgcolor = sco_color[param1 & 15];
            break;
        default:
        unhandled:
            TRACE_MSG(s, "unhandled");
            break;
        }
        break;
    }
#undef ESC2
done:
    qe_term_update_cursor(s);
}

/* buffer related functions */

/* called when characters are available from the process */
static void shell_read_cb(void *opaque)
{
    ShellState *s = opaque;
    QEmacsState *qs;
    EditBuffer *b;
    unsigned char buf[16 * 1024];
    int len, i, save_readonly;
    int prev_offset;

    if (!s || s->base.mode != &shell_mode)
        return;

    len = read(s->pty_fd, buf, sizeof(buf));
    if (len <= 0) {
        if (s->pid == -1)
            shell_close(s);
        return;
    }

    b = s->b;
    qs = s->base.qs;
    if (qs->trace_buffer)
        qe_trace_bytes(qs, buf, len, EB_TRACE_SHELL);

    /* Suspend BF_READONLY flag to allow shell output to readonly buffer */
    save_readonly = b->flags & BF_READONLY;
    b->flags &= ~BF_READONLY;
    prev_offset = b->offset = b->total_size;
    b->last_log = 0;

    if (s->shell_flags & SF_COLOR) {
        /* optional terminal emulation (shell, ssh, make, latex, man modes) */
        for (i = 0; i < len; i++) {
            qe_term_emulate(s, buf[i]);
        }
        if (s->last_char == '\000' || s->last_char == '\001'
        ||  s->last_char == '\003'
        ||  s->last_char == '\r' || s->last_char == '\n') {
            /* if the last char sent to the process was the enter key, C-C
             * to kill the process, C-A to go to beginning of line, or if
             * nothing was sent to the process yet, assume the process is
             * prompting for input and save the current input position as
             * the start of input.
             */
            s->cur_prompt = s->cur_offset;
            if (qs->active_window
            &&  qs->active_window->b == b
            &&  qs->active_window->interactive) {
                /* Set mark to potential tentative position (useful?) */
                b->mark = s->cur_prompt;
            }
        }
    } else {
        int pos = b->total_size;
        int threshold = 3 << 20;    /* 3MB for large pictures */
        eb_write(b, b->total_size, buf, len);
        if (pos < threshold && pos + len >= threshold) {
            EditState *e;
            for (e = qs->first_window; e != NULL; e = e->next_window) {
                if (e->b == b) {
                    if (s->shell_flags & SF_AUTO_CODING)
                        do_set_auto_coding(e, 0);
                    if (s->shell_flags & SF_AUTO_MODE)
                        qe_set_next_mode(e, 0, 0);
                }
            }
        }
    }
    if (save_readonly) {
        b->modified = 0;
        b->flags |= BF_READONLY;
    }

    EditState *e = qs->active_window;
    if (e->b == b && !(s->shell_flags & SF_NO_DRAG)) {
        if ((e->offset == prev_offset) || (qs->last_cmd_func == (CmdFunc)do_eof))
            e->offset = b->total_size;
    }

    /* now we do some refresh (should just invalidate?) */
    qe_display(qs);
}

static void shell_mode_free(EditBuffer *b, void *state)
{
    ShellState *s = state;
    int tries = 5, sig, rc, status;

    if (s == NULL)
        return;

    eb_free_callback(b, eb_offset_callback, &s->cur_offset);
    eb_free_callback(b, eb_offset_callback, &s->cur_prompt);
    eb_free_callback(b, eb_offset_callback, &s->alternate_screen_top);
    eb_free_callback(b, eb_offset_callback, &s->screen_top);

    if (s->pid != -1) {
        sig = SIGINT;
        while (tries --> 0) {
            /* try and kill the child process */
            kill(s->pid, sig);
            /* wait first 100 ms */
            usleep(100 * 1000);
            rc = waitpid(s->pid, &status, WNOHANG);
            if (rc < 0 && errno == ECHILD)
                break;
            if (rc == s->pid) {
                /* if process exited or was killed, all good */
                if (WIFEXITED(status) || WIFSIGNALED(status))
                    break;
            }
            /* if still not killed, then try harder (useful for shells) */
            sig = SIGKILL;
        }
        url_set_pid_handler(b->qs->up, s->pid, NULL, NULL);
        s->pid = -1;
    }
    if (s->pty_fd >= 0) {
        url_set_read_handler(b->qs->up, s->pty_fd, NULL, NULL);
        close(s->pty_fd);
        s->pty_fd = -1;
    }
}

static void shell_pid_cb(void *opaque, int status)
{
    ShellState *s;
    EditBuffer *b;
    QEmacsState *qs;
    EditState *e;

    /* Extra check in case mode data was freed already.
     * It is not completely fool proof as the same address might have
     * been reallocated for the same purpose, quite unlikely.
     */
    s = check_mode_data(&opaque);
    if (!s || s->base.mode != &shell_mode)
        return;

    b = s->b;
    qs = s->base.qs;
    url_set_pid_handler(qs->up, s->pid, NULL, NULL);
    s->exit_status = status;
    s->pid = -1;

    /* remove shell input mode */
    s->grab_keys = 0;
    qe_ungrab_keys(qs);
    for (e = qs->first_window; e != NULL; e = e->next_window) {
        if (e->b == b)
            e->interactive = 0;
    }
}

static void shell_close(ShellState *s)
{
    QEmacsState *qs = s->base.qs;
    EditBuffer *b = s->b;
    int status = s->exit_status;
    char buf[1024];
    EditState *e;

    *buf = '\0';
    if (s->caption) {
        time_t ti;
        char *time_str;

        ti = time(NULL);
        time_str = ctime(&ti);
        if (WIFEXITED(status)) {
            int code = WEXITSTATUS(status);
            if (code == 0) {
                snprintf(buf, sizeof(buf), "%s finished", s->caption);
            } else {
                snprintf(buf, sizeof(buf), "%s exited abnormally with code %d",
                         s->caption, code);
            }
        } else if (WIFSIGNALED(status)) {
            snprintf(buf, sizeof(buf), "%s %s",
                     s->caption, strsignal(WTERMSIG(status)));
        }
        if (*buf != '\0') {
            int save_readonly = b->flags & BF_READONLY;
            int prev_offset;

            e = qs->active_window;
            if (!(s->shell_flags & SF_INTERACTIVE))
                put_status(e, "%s", buf);

            /* Flush output to buffer, bypassing readonly flag */
            b->flags &= ~BF_READONLY;

            prev_offset = b->offset = b->total_size;
            eb_printf(b, "\n%s at %s", buf, time_str);

            if (e->b == b) {
                if (e->interactive || (e->offset == prev_offset)
                || (qs->last_cmd_func == (CmdFunc)do_eof)) {
                    e->offset = b->total_size;
                }
            }

            if (save_readonly) {
                b->modified = 0;
                b->flags |= BF_READONLY;
            }
        }
    }

    if (s->pty_fd >= 0) {
        url_set_read_handler(qs->up, s->pty_fd, NULL, NULL);
        close(s->pty_fd);
        s->pty_fd = -1;
    }

    for (e = qs->first_window; e != NULL; e = e->next_window) {
        if (e->b == b) {
            if (s->shell_flags & SF_AUTO_CODING)
                do_set_auto_coding(e, 0);
            if (s->shell_flags & SF_AUTO_MODE)
                qe_set_next_mode(e, 0, 0);
            if (qs->shell_buffer_read_only) {
                if (s->shell_flags & SF_INTERACTIVE)
                    edit_set_mode(e, &compilation_mode);
            }
        }
    }

    /* Free the shell data safely */
    if (!(s->shell_flags & SF_INTERACTIVE)) {
        qe_free_mode_data(&s->base);
    }

    qe_display(qs);
}

EditBuffer *qe_new_shell_buffer(QEmacsState *qs, EditBuffer *b0, EditState *e,
                                const char *bufname, const char *caption,
                                const char *path, const char *cmd,
                                int shell_flags)
{
    ShellState *s;
    EditBuffer *b;
    const char *lang;
    int cols, rows;
    char curpath[MAX_FILENAME_SIZE];

    if (!b0 && (shell_flags & SF_REUSE_BUFFER)) {
        b0 = qe_find_buffer_name(qs, bufname);
        if (b0) {
            if (shell_flags & SF_ERASE_BUFFER) {
                /* XXX: this also clears the undo records */
                eb_clear(b0);
            }
        }
    }

    b = b0;
    if (!b) {
        b = qe_new_buffer(qs, bufname, BF_SAVELOG | BF_SHELL);
        if (!b)
            return NULL;
    }
    shell_flags &= ~(SF_REUSE_BUFFER | SF_ERASE_BUFFER);

    // Set up filename and initial directory property
    if (path) {
        pstrcpy(curpath, countof(curpath), path);
    } else
    if (e) {
        get_default_path(e->b, e->offset, curpath, countof(curpath));
    } else {
        *curpath = '\0';
        getcwd(curpath, countof(curpath));
    }
    append_slash(curpath, countof(curpath));
    shell_add_cwd(b, 0, curpath, 1);

    eb_set_buffer_name(b, bufname); /* ensure that the name is unique */
    if (shell_flags & SF_COLOR) {
        eb_create_style_buffer(b, BF_STYLE_COMP);
    }
    /* Select shell output buffer encoding from LANG setting */
    if (((lang = getenv("LANG")) != NULL && strstr(lang, "UTF-8")) ||
          qs->screen->charset == &charset_utf8) {
        eb_set_charset(b, &charset_utf8, b->eol_type);
    } else {
        eb_set_charset(b, &charset_vt100, b->eol_type);
    }

    s = qe_get_buffer_mode_data(b, &shell_mode, NULL);
    if (!s) {
        s = (ShellState*)qe_create_buffer_mode_data(b, &shell_mode);
        if (!s) {
            if (!b0)
                eb_free(&b);
            return NULL;
        }
        /* Track cursor with edge effect */
        eb_add_callback(b, eb_offset_callback, &s->cur_offset, 1);
        eb_add_callback(b, eb_offset_callback, &s->cur_prompt, 0);
        eb_add_callback(b, eb_offset_callback, &s->alternate_screen_top, 0);
        eb_add_callback(b, eb_offset_callback, &s->screen_top, 0);
    }
    s->b = b;
    s->pty_fd = -1;
    s->pid = -1;
    s->exit_status = -1;
    s->caption = caption;
    s->shell_flags = shell_flags;
    s->cur_prompt = s->cur_offset = b->total_size;
    qe_term_init(s);

    /* launch shell */
    /* default values for cols, rows should come from the screen size */
    cols = QE_TERM_XSIZE;
    rows = QE_TERM_YSIZE;
    if (e) {
        cols = e->cols;
        rows = e->rows;
    }
    s->cols = cols;
    s->rows = rows;

    if (run_process(s, cmd, &s->pty_fd, &s->pid, cols, rows, path, shell_flags) < 0) {
        if (!b0)
            eb_free(&b);
        return NULL;
    }

    /* XXX: ShellState life cycle is bogus */
    url_set_read_handler(qs->up, s->pty_fd, shell_read_cb, s);
    url_set_pid_handler(qs->up, s->pid, shell_pid_cb, s);
    return b;
}

/* If a window is attached to buffer b, activate it,
   otherwise attach window s to buffer b.
   *sp is not NULL.
 */
static EditBuffer *try_show_buffer(EditState **sp, const char *bufname)
{
    EditState *e, *s = *sp;
    QEmacsState *qs = s->qs;
    EditBuffer *b;

    b = qe_find_buffer_name(qs, bufname);
    if (b && s->b != b) {
        e = eb_find_window(b, NULL);
        if (e) {
            qs->active_window = *sp = e;
        } else {
            switch_to_buffer(s, b);
        }
    }
    return b;
}

static void do_shell(EditState *e, int argval)
{
    /*@CMD shell
       ### `shell(int argval)`

       Start a shell buffer or move to the last shell buffer used.
       Unless an argument different from one is provided, the command attempts
       to switch to an existing, active shell buffer. If the process has
       already terminated, it will restart the shell in the original directory.
       If no shell buffer exists, or argval is not equal to one, it starts a
       new shell in the default directory of the current window.

       ### Current Working Directory Synchronization

       QEmacs supports tracking the current working directory of the shell
       through OSC 7 escape sequences. This allows commands such as
       `next-error` to resolve relative file names against the directory
       reported by the shell.

       To enable this feature, add an OSC 7 hook to your shell's startup
       file. The examples below emit an OSC 7 sequence before each prompt
       when running inside QEmacs:

       **Bash:**
       ```bash
       if [ "$TERM_PROGRAM" = "qemacs" ]; then
           export PROMPT_COMMAND='echo -ne "\e]7;file://${PWD}\a"'
       fi
       ```
       **Zsh:**
       ```zsh
       if [ "$TERM_PROGRAM" = "qemacs" ]; then
           function precmd() {
               builtin print -Pn "\e]7;file://${PWD}\a"
           }
       fi
       ```
       **Fish:**
       ```fish
       if test "$TERM_PROGRAM" = "qemacs"
           function sync_qemacs_shell --on-event=fish_prompt
               printf "\e]7;file://$PWD\a"
           end
       end
       ```
       Other shells can be configured similarly by emitting:
       `ESC ] 7 ; file://cwd BEL`

       whenever the prompt is displayed or the current working
       directory changes.
     */
    char curpath[MAX_FILENAME_SIZE];
    EditBuffer *b = NULL;

    if (e->flags & (WF_POPUP | WF_MINIBUF))
        return;

    /* Start the shell in the default directory of the current window */
    get_default_path(e->b, e->offset, curpath, sizeof curpath);

    /* avoid messing with the dired pane */
    e = qe_find_target_window(e, 1);

    /* find shell buffer if any */
    if (argval == 1) {
        if (strstart(e->b->name, "*shell", NULL)) {
            /* If the current buffer is a shell buffer, use it */
            b = e->b;
        } else {
            /* Find the last used shell buffer, if any */
            if (strstart(error_state.buffer, "*shell", NULL)) {
                b = try_show_buffer(&e, error_state.buffer);
            }
            if (b == NULL) {
                b = try_show_buffer(&e, "*shell*");
            }
        }
        if (b) {
            /* If the process is active, switch to interactive mode */
            ShellState *s = shell_get_state(e, 0);
            if (s && s->pid >= 0) {
                e->offset = b->total_size;
                if ((s->shell_flags & SF_INTERACTIVE) && !s->grab_keys) {
                    e->offset = s->cur_offset;
                    e->interactive = 1;
                    e->b->flags &= ~BF_READONLY;
                }
                return;
            }
            /* otherwise, restart the process here */
            e->offset = b->total_size;
            /* restart the shell in the same directory */
            get_default_path(e->b, e->offset, curpath, sizeof curpath);
            if (e->qs->shell_buffer_read_only) {
                /* set mode back to shell mode */
                edit_set_mode(e, &shell_mode);
                /* make sure readonly is not set */
                b->flags &= ~BF_READONLY;
            }
        }
    }

    /* create new shell buffer or restart shell in current buffer */
    b = qe_new_shell_buffer(e->qs, b, e, "*shell*", "Shell process",
                            curpath, NULL, SF_COLOR | SF_INTERACTIVE);
    if (!b)
        return;

    b->flags |= BF_ERROR;
    b->default_mode = &shell_mode;
    switch_to_buffer(e, b);
    /* force interactive mode if restarting */
    shell_mode.mode_init(e, b, 0);
    do_shell_refresh(e, SR_UPDATE_SIZE | SR_SILENT);
    set_error_offset(b, 0);
    put_status(e, "Press C-o to toggle between shell input and buffer navigation");
}

static EditState *shell_target_window(EditState *e, EditBuffer *b)
{
    /* Shell: target window selection logic

       The behavior follows this priority order:

       1. reuse an existing window already displaying the buffer,
       2. if shell-command-other-window is not set, use the current
          window, otherwise
       3. select the largest available non-active window.
       4. split a new window if no sutiable window is found.
       5. fall back to the current window if splitting fails.
     */
    ShellState *s = qe_get_buffer_mode_data(b, &shell_mode, NULL);
    QEmacsState *qs = e->qs;
    EditState *e1 = NULL;

    if (s && !(s->shell_flags & SF_INTERACTIVE)) {
        e1 = eb_find_window(b, NULL);
    }

    if (e1 == NULL && qs->shell_command_other_window) {
        int size1 = 0;
        // Try and locate the largest window
        EditState *e2;
        for (e2 = qs->first_window; e2 != NULL; e2 = e2->next_window) {
            if (e2->flags & (WF_POPUP | WF_MINIBUF | WF_POPLEFT))
                continue;
            if (e2 != qs->active_window) {
                int size2 = e2->width * e2->height;
                if (size2 > size1) {
                    size1 = size2;
                    e1 = e2;
                }
            }
        }

        if (e1 == NULL) {
            e1 = qe_split_window(e, SW_STACKED, 50);
            if (e1 == NULL) {
                e1 = e;
            } else {
                switch_to_buffer(e1, NULL);
                e1->last_buffer = NULL;
            }
        }
    }

    if (e1 == NULL)
        e1 = e;

    switch_to_buffer(e1, b);
    return e1;
}

static void do_man(EditState *s, const char *arg)
{
    char bufname[MAX_BUFFERNAME_SIZE];
    char cmd[16 + MAX_FILENAME_SIZE];
    QEmacsState *qs = s->qs;
    EditBuffer *b;

    if (s->flags & (WF_POPUP | WF_MINIBUF))
        return;

    if (s->flags & WF_POPLEFT) {
        /* avoid messing with the dired pane */
        s = find_window(s, KEY_RIGHT, s);
        qs->active_window = s;
    }

    // keep formatting with environment variable for systems where `man`
    // disables formatting when writing to a pipe or a file.
    snprintf(cmd, sizeof(cmd), "MAN_KEEP_FORMATTING=1 man %s", arg);

    snprintf(bufname, sizeof(bufname), "*Man %s*", arg);
    if (try_show_buffer(&s, bufname))
        return;

    /* create new buffer */
    b = qe_new_shell_buffer(qs, NULL, s, bufname, NULL, NULL, cmd, SF_COLOR | SF_NO_DRAG);
    if (!b)
        return;

    b->data_type_name = "man";
    s = shell_target_window(s, b);
    edit_set_mode(s, &pager_mode);
}

void qe_diff_buffer_with_file(EditState *s, EditBuffer *b)
{
    char bufname[MAX_BUFFERNAME_SIZE];
    char cmd[64 + 2 * MAX_FILENAME_SIZE];
    char tmpfile[MAX_FILENAME_SIZE];
    QEmacsState *qs = s->qs;
    EditState *e;
    EditBuffer *b1;
    const char *fname = get_basename(b->filename);
    const char *tmpdir = getenv("TMPDIR");
    int tmpdir_len;

    if (s->flags & (WF_POPUP | WF_MINIBUF))
        return;

    if (!*fname)
        return;

    if (!tmpdir || !*tmpdir) tmpdir = "/tmp";
    tmpdir_len = strlen(tmpdir);
    if (tmpdir[tmpdir_len - 1] == '/') tmpdir_len--;

    snprintf(tmpfile, sizeof(tmpfile), "%.*s/qe-%d-%s",
             tmpdir_len, tmpdir, getpid(), fname);
    eb_write_buffer(b, 0, b->total_size, tmpfile);

    // Pass options to diff command
    // -s, --report-identical-files
    //      report when two files are the same
    // -p, --show-c-function
    //      show which C function each change is in
    // --label LABEL   [-L on FreeBSD and OpenBSD]
    //      use LABEL instead of file name and timestamp (can be repeated)
    // -t, --expand-tabs
    //      expand tabs to spaces in output
    // --color[=WHEN]  [macOS only]
    //      color output; WHEN is 'never', 'always', or 'auto'; plain
    //      --color means --color='auto'

#if defined(CONFIG_DARWIN) || defined(CONFIG_LINUX)
    snprintf(cmd, sizeof(cmd), "diff -s -p -u --label '%s' --label '#<buffer %s>' '%s' '%s'; rm '%s'",
             b->filename, b->name, b->filename, tmpfile, tmpfile);
#elif defined(CONFIG_FREEBSD) || defined(CONFIG_OPENBSD)
    snprintf(cmd, sizeof(cmd), "diff -s -p -u -L '%s' -L '#<buffer %s>' '%s' '%s'; rm '%s'",
             b->filename, b->name, b->filename, tmpfile, tmpfile);
#else
    snprintf(cmd, sizeof(cmd), "diff -s -p -u '%s' '%s'; rm '%s'",
             b->filename, tmpfile, tmpfile);
#endif
    snprintf(bufname, sizeof(bufname), "*Diff %s*", fname);

    b1 = qe_new_shell_buffer(qs, NULL, s, bufname, NULL, NULL, cmd, SF_COLOR | SF_NO_DRAG);
    if (!b1) {
        unlink(tmpdir);
        return;
    }

    // construct popup caption
    snprintf(cmd, sizeof(cmd), "Diff buffer %s", fname);

    b1->data_type_name = "diff";
    e = show_popup(s, b1, cmd);
    // XXX: Should use patch mode
    edit_set_mode(e, &pager_mode);
}

static void do_diff_buffer_with_file(EditState *s)
{
    qe_diff_buffer_with_file(s, s->b);
}

static void do_ssh(EditState *s, const char *arg)
{
    char bufname[MAX_BUFFERNAME_SIZE];
    char cmd[16 + MAX_FILENAME_SIZE];
    QEmacsState *qs = s->qs;
    EditBuffer *b;

    if (s->flags & (WF_POPUP | WF_MINIBUF))
        return;

    if (s->flags & WF_POPLEFT) {
        /* avoid messing with the dired pane */
        s = find_window(s, KEY_RIGHT, s);
        qs->active_window = s;
    }

    /* Use standard ssh command */
    snprintf(cmd, sizeof(cmd), "ssh %s", arg);
    snprintf(bufname, sizeof(bufname), "*ssh-%s*", arg);

    /* create new buffer */
    b = qe_new_shell_buffer(qs, NULL, s, bufname, "ssh",
                            NULL, cmd, SF_COLOR | SF_INTERACTIVE);
    if (!b)
        return;

    b->data_type_name = "ssh";
    b->default_mode = &shell_mode;
    switch_to_buffer(s, b);

    put_status(s, "Press C-o to toggle between shell input and buffer navigation");
}

static void shell_move_left_right(EditState *e, int dir)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        qe_term_write(s, dir > 0 ? s->kcuf1 : s->kcub1, -1);
    } else {
        text_move_left_right_visual(e, dir);
    }
}

static void shell_move_word_left_right(EditState *e, int dir)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        qe_term_write(s, dir > 0 ? "\033f" : "\033b", -1);
    } else {
        text_move_word_left_right(e, dir);
    }
}

static void shell_maybe_toggle_interactive(EditState *e)
{
    ShellState *s = shell_get_state(e, 1);

    /* optionally restore shell interactive mode if point is at end */
    if (!e->interactive && !e->region_style
    &&  e->qs->shell_mode_auto_interactive
    &&  s && (s->shell_flags & SF_INTERACTIVE) && !s->grab_keys) {
        if (e->offset >= e->b->total_size && e->offset >= s->cur_offset) {
            e->interactive = 1;
            e->b->flags &= ~BF_READONLY;
            if (e->offset > s->cur_offset)
                qe_term_write(s, "\005", 1); /* Control-E */
        }
    }
}

static void shell_move_up_down(EditState *e, int dir)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        qe_term_write(s, dir > 0 ? s->kcud1 : s->kcuu1, -1);
    } else {
        text_move_up_down(e, dir);
        shell_maybe_toggle_interactive(e);
    }
}

static void shell_previous_next(EditState *e, int dir)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        /* hack: M-p silently converted to C-p */
        qe_term_write(s, dir > 0 ? s->kcud1 : s->kcuu1, -1);
    } else {
        /* hack: M-p silently converted to C-u C-p */
        text_move_up_down(e, dir * 4);
        shell_maybe_toggle_interactive(e);
    }
}

static void shell_exchange_point_and_mark(EditState *e)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        qe_term_write(s, "\030\030", 2);  /* C-x C-x */
    } else {
        do_exchange_point_and_mark(e);
        shell_maybe_toggle_interactive(e);
    }
}

static void shell_scroll_up_down(EditState *e, int dir)
{
    e->interactive = 0;
    if (e->qs->shell_buffer_read_only)
        e->b->flags |= BF_READONLY;
    text_scroll_up_down(e, dir);
    shell_maybe_toggle_interactive(e);
}

static void shell_move_bol(EditState *e)
{
    ShellState *s = shell_get_state(e, 1);

    if (e->qs->shell_mode_auto_interactive) {
        /* exit shell interactive mode on home / ^A at start of shell input */
        if (!s || (e->offset == s->cur_prompt && !s->grab_keys))
            e->interactive = 0;
    }

    if (s && e->interactive) {
        qe_term_write(s, "\001", 1); /* Control-A */
    } else {
        text_move_bol(e);
    }
}

static void shell_move_eol(EditState *e)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        qe_term_write(s, "\005", 1); /* Control-E */
    } else {
        text_move_eol(e);
        shell_maybe_toggle_interactive(e);
    }
}

static void shell_move_bof(EditState *e)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        if (e->qs->shell_mode_auto_interactive) {
            /* Exit shell interactive mode on home-buffer / M-< */
            e->interactive = 0;
            text_move_bof(e);
        } else {
            qe_term_write(s, "\001", 1); /* Control-A */
        }
    } else {
        text_move_bof(e);
    }
}

static void shell_move_eof(EditState *e)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        qe_term_write(s, "\005", 1); /* Control-E */
    } else {
        text_move_eof(e);
        shell_maybe_toggle_interactive(e);
    }
}

static void shell_write_char(EditState *e, int c)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        char buf[10];
        int len;

        if (c >= KEY_META(0) && c <= KEY_META(0xff)) {
            buf[0] = '\033';
            buf[1] = c - KEY_META(0);
            len = 2;
        } else {
            len = eb_encode_char32(e->b, buf, c);
        }
        qe_term_write(s, buf, len);
    } else {
        text_write_char(e, c);
    }
}

static void shell_delete_bytes(EditState *e, int offset, int size)
{
    ShellState *s = shell_get_state(e, 1);
    int start = offset;
    int end = offset + size;

    // XXX: should deal with regions spanning current input line and
    // previous buffer contents
    if (s && !s->grab_keys && end > s->cur_prompt) {
        int start_char, cur_char, end_char, size1;
        if (start < s->cur_prompt) {
            /* delete part before the interactive input */
            size1 = eb_delete_range(e->b, start, s->cur_prompt);
            end -= size1;
            start = s->cur_prompt;
        }
        start_char = eb_get_char_offset(e->b, start);
        cur_char = eb_get_char_offset(e->b, s->cur_offset);
        end_char = eb_get_char_offset(e->b, end);
        if (start == s->cur_prompt && cur_char > start_char + 2) {
            qe_term_write(s, "\001", 1);  /* C-a */
            cur_char = start_char;
        }
        while (cur_char > end_char) {
            qe_term_write(s, "\002", 1);  /* C-b */
            cur_char--;
        }
        while (cur_char < start_char) {
            qe_term_write(s, "\006", 1);  /* C-f */
            cur_char++;
        }
        if (start_char == cur_char && end == e->b->total_size) {
            /* kill to end of line with control-k */
            qe_term_write(s, "\013", 1);
        } else {
            /* Do not use C-d to delete characters because C-d may exit shell */
            while (cur_char < end_char) {
                qe_term_write(s, "\006", 1);  /* C-f */
                cur_char++;
            }
            while (start_char < cur_char) {
                qe_term_write(s, "\010", 1);  /* backspace */
                cur_char--;
                end_char--;
            }
        }
    } else {
        eb_delete(e->b, offset, size);
    }
}

static void do_shell_newline(EditState *e)
{
    if (e->interactive) {
        struct timespec ts;

        if (qe_save_buffers(e->qs, SBF_EXAMINE))
            return;
        shell_write_char(e, '\r');
        /* give the process a chance to handle the input */
        ts.tv_sec = 0;
        ts.tv_nsec = 1000000;  /* 1 ms */
        nanosleep(&ts, NULL);
    } else {
        do_newline(e);
    }
    /* reset offset to scan errors and matches from */
    set_error_offset(e->b, e->offset);
}

static void do_shell_intr(EditState *e)
{
    if (e->interactive) {
        shell_write_char(e, 3);
    } else {
        text_write_char(e, 3);
    }
}

static void do_shell_delete_char(EditState *e)
{
    if (e->interactive) {
        shell_write_char(e, 4);
    } else {
        do_delete_char(e, NO_ARG);
    }
}

static void do_shell_backspace(EditState *e)
{
    if (e->interactive) {
        shell_write_char(e, KEY_DEL);
    } else {
        do_backspace(e, NO_ARG);
    }
}

static void do_shell_search(EditState *e, int dir)
{
    if (e->interactive) {
        shell_write_char(e, dir < 0 ? ('r' & 31) : ('s' & 31));
    } else {
        do_isearch(e, NO_ARG, dir);
    }
}

static void do_shell_kill_word(EditState *e, int dir)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        /* copy word to the kill ring */
        int start = e->offset;

        // XXX: word pattern is different for shell line editor?
        text_move_word_left_right(e, dir);
        if (e->offset < s->cur_prompt) {
            e->offset = s->cur_prompt;
        }
        do_kill(e, start, e->offset, dir, 1);
        // XXX: word pattern consistency issue
        shell_write_char(e, dir > 0 ? KEY_META('d') : KEY_META(KEY_DEL));
    } else {
        do_kill_word(e, dir);
    }
}

static void do_shell_kill_line(EditState *e, int argval)
{
    ShellState *s = shell_get_state(e, 1);
    int dir = (argval == NO_ARG || argval > 0) ? 1 : -1;
    int offset, p1 = e->offset, p2 = p1;

    if (s && e->interactive) {
        /* ignore count argument in interactive mode */
        if (dir < 0) {
            /* kill backwards upto prompt position */
            p2 = max_offset(eb_goto_bol(e->b, p1), s->cur_prompt);
            do_kill(e, p1, p2, dir, 0);
            //shell_write_char(e, KEY_META('k'));
        } else {
            p2 = eb_goto_eol(e->b, p1);
            do_kill(e, p1, p2, dir, 0);
            //shell_write_char(e, KEY_CTRL('k'));
        }
    } else {
        /* Cannot use do_kill_line() because it relies on mode specific
         * cursor movement methods, which are handled asynchronously in
         * shell mode.
         */
        if (argval == NO_ARG) {
            /* kill to end of line */
            if (eb_nextc(e->b, p2, &offset) == '\n') {
                p2 = offset;
            } else {
                p2 = eb_goto_eol(e->b, p2);
            }
        } else
        if (argval <= 0) {
            /* kill backwards */
            dir = -1;
            for (;;) {
                p2 = eb_goto_bol(e->b, p2);
                if (p2 <= 0 || argval == 0)
                    break;
                p2 = eb_prev(e->b, p2);
                argval += 1;
            }
        } else {
            for (;;) {
                p2 = eb_goto_eol(e->b, p2);
                if (p2 >= e->b->total_size || argval == 0)
                    break;
                p2 = eb_next(e->b, p2);
                argval -= 1;
            }
        }
        e->offset = p2;
        do_kill(e, p1, p2, dir, 0);
    }
}

static void do_shell_kill_beginning_of_line(EditState *s, int argval)
{
    do_shell_kill_line(s, argval == NO_ARG ? 0 : -argval);
}

// XXX: need shell_set_mark, shell_kill_region...

static void do_shell_yank(EditState *e)
{
    if (e->interactive) {
        /* yank from kill-ring and insert via shell_write_char().
         * This will cause a deadlock if kill buffer contents is too
         * large. Hard coded limit can be removed if shell input is
         * made asynchronous via an auxiliary buffer.
         */
        int offset;
        QEmacsState *qs = e->qs;
        EditBuffer *b = qs->yank_buffers[qs->yank_current];

        e->b->mark = e->offset;

        if (b) {
            if (b->total_size > 1024) {
                put_error(e, "Too much data to yank at shell prompt");
                return;
            }
            for (offset = 0; offset < b->total_size;) {
                char32_t c = eb_nextc(b, offset, &offset);
                if (c == '\n')
                    do_shell_newline(e);
                else
                    shell_write_char(e, c);
            }
        }
        qs->this_cmd_func = (CmdFunc)do_yank;
    } else {
        do_yank(e);
    }
}

static void do_shell_changecase_word(EditState *e, int dir)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        // XXX: word pattern consistency issue
        shell_write_char(e, dir == 2 ? KEY_META('c')
                         : dir < 0 ? KEY_META('l')
                         : KEY_META('u'));
    } else {
        do_changecase_word(e, dir);
    }
}

static void do_shell_transpose(EditState *e, int cmd)
{
    ShellState *s = shell_get_state(e, 1);

    if (s && e->interactive) {
        shell_write_char(e, cmd == CMD_TRANSPOSE_CHARS ?
                         KEY_CTRL('T') : KEY_META('t'));
    } else {
        do_transpose(e, cmd);
    }
}

static void do_shell_tabulate(EditState *e)
{
    if (e->interactive) {
        shell_write_char(e, 9);
    } else {
        //do_tabulate(s, 1);
        text_write_char(e, 9);
    }
}

static void do_shell_refresh(EditState *e, int flags)
{
    ShellState *s;

    if ((s = shell_get_state(e, 1)) != NULL) {
        QEmacsState *qs = e->qs;
        EditState *e1;

        /* update the terminal size and notify process */
        s->cols = e->wrap_cols = e->cols;
        s->rows = e->rows;

        for (e1 = qs->first_window; e1 != NULL; e1 = e1->next_window) {
            if (e1->b == e->b) {
                e1->wrap_cols = s->cols;
            }
        }

        if (s->pty_fd > 0 && (flags & SR_UPDATE_SIZE)) {
            struct winsize ws;
            ioctl(s->pty_fd, TIOCGWINSZ, &ws);
            ws.ws_col = s->cols;
            ws.ws_row = s->rows;
            ioctl(s->pty_fd, TIOCSWINSZ, &ws);
        }
    }
    if (flags & SR_REFRESH)
        do_refresh_complete(e);
    if (s && !(flags & SR_SILENT)) {
        put_status(e, "Terminal size set to %d by %d", s->cols, s->rows);
    }
}

static void do_shell_toggle_input(EditState *e)
{
    ShellState *s;
    QEmacsState *qs = e->qs;
    EditState *e1;

    if ((s = shell_get_state(e, 1)) != NULL) {
        if (e->interactive) {
            e->interactive = 0;
            if (qs->shell_buffer_read_only) {
                e->b->flags |= BF_READONLY;
                for (e1 = qs->first_window; e1 != NULL; e1 = e1->next_window) {
                    if (e1->b == e->b)
                        e1->interactive = 0;
                }
            }
            return;
        } else {
            if (s->shell_flags & SF_INTERACTIVE) {
                e->b->flags &= ~BF_READONLY;
                e->interactive = 1;
                e->offset = s->cur_offset;
                if (s->grab_keys)
                    qe_grab_keys(s->base.qs, shell_key, s);
                if (qs->shell_buffer_read_only) {
                    for (e1 = qs->first_window; e1 != NULL; e1 = e1->next_window) {
                        if (e1->b == e->b) {
                            if (e->offset == s->cur_offset)
                                e->interactive = 1;
                        }
                    }
                }
            }
        }
    } else {
        do_open_line(e);
    }
}

/* check shell output for prompt and track current directory */
static int shell_check_curpath(ShellState *s, int offset, int trigger)
{
    char line[1024];
    char curpath[MAX_FILENAME_SIZE];
    EditBuffer *b = s->b;
    int bol, start, stop0, stop, i, len, offset1;

    bol = eb_goto_bol(b, offset);
    len = eb_fgets(b, line, countof(line), bol, &offset1);
    line[len] = '\0';   /* strip the trailing newline if any */

    if (trigger == '\n') {
        // Track make -w -C messages about temporary directory excursion
        const char *p;
        if ((p = strstr(line, "Entering directory ")) != NULL) {
            // Push new directory onto the stack and add a property
            ShellCwdEntry *cwdp;
            p += strlen("Entering directory ");
            if (*p == '`') p++;
            start = p - line;
            for (stop = start; line[stop] && line[stop] != '\''; stop++)
                continue;
            line[stop] = '\0';
            canonicalize_absolute_buffer_path(s->b, bol, curpath, countof(curpath), p);
            append_slash(curpath, countof(curpath));
            if (s->cwd_stack_depth == countof(s->cwd_stack)) {
                memmove(s->cwd_stack, s->cwd_stack + 1,
                        sizeof(*s->cwd_stack) * (countof(s->cwd_stack) - 1));
                s->cwd_stack_depth--;
            }
            cwdp = &s->cwd_stack[s->cwd_stack_depth++];
            cwdp->error_count = 0;
            cwdp->prop = shell_add_cwd(b, offset1, curpath, 1);
            return 1;
        }
        if ((p = strstr(line, "Leaving directory ")) != NULL) {
            // Pop directory from the stack and delete the property if no errors
            if (s->cwd_stack_depth) {
                ShellCwdEntry *cwdp = &s->cwd_stack[--s->cwd_stack_depth];
                QEProperty *prev = eb_find_property(b, 0, cwdp->prop->offset,
                                                    QE_PROP_CWD, cwdp->prop);
                if (prev) {
                    if (!cwdp->error_count)
                        eb_del_property(b, cwdp->prop);
                    shell_add_cwd(b, offset1, prev->data, 0);
                }
            }
            return 1;
        }
        // Check for error message:
        // - maybe parse errors at this time to track error location
        //   for files that are loaded already, especially if the buffer
        //   is out of sync with the corresponding file.
        if (match_error(b, bol, NULL)) {
            if (s->cwd_stack_depth) {
                s->cwd_stack[--s->cwd_stack_depth].error_count += 1;
            }
            return 1;
        }
        return 0;
    }

    // Parse shell prompt for current directory
    start = stop = stop0 = 0;
    for (i = 0; i < len;) {
        char c = line[i++];
        if (c == '#' || c == '$' || c == '>') {
            stop = stop0;
            break;
        }
        if (c == ':' && line[i] != '\\'
        &&  !(line[i] == '/' && line[i + 1] == '/')
        &&  !(line[i] == '/' && line[i + 1] == '*'))
            start = i;
        if (c == ' ') {
            if (!start || start == i - 1)
                start = i;
        } else {
            stop0 = i;
        }
    }
    if (stop > start) {
        line[stop] = '\0';
        /* XXX: should use a lower level function to avoid potential recursion */
        canonicalize_absolute_path(NULL, curpath, countof(curpath), line + start);
        if (is_directory(curpath)) {
            append_slash(curpath, countof(curpath));
            shell_add_cwd(b, bol, curpath, 0);
            return 1;
        }
    }
    return 0;
}

static char *shell_get_default_path(EditBuffer *b, int offset,
                                    char *buf, int buf_size)
{
    QEProperty *p = eb_find_property(b, 0, offset, QE_PROP_CWD, NULL);
    return pstrcpy(buf, buf_size, p ? p->data : b->filename);
}

static void shell_kill_process_confirm_cb(void *opaque, char *reply, CompletionDef *completion)
{
    ShellKillContext *sc = opaque;
    int yes_replied;

    if (reply) {
        yes_replied = strequal(reply, "yes");
        qe_free(&reply);
        if (!yes_replied)
            goto end;
        shell_mode_free(sc->b, sc->s);
        sc->callback(sc->e, sc->cmd);
    }

end:
    qe_free(&sc->cmd);
    qe_free(&sc);
}

static int shell_check_active_process(QEmacsState *qs, const char *bufname,
                                      void (*cb)(EditState*, const char*),
                                      EditState *e, const char *cmd)
{
    EditBuffer *b;
    char buf[256];

    b = qe_find_buffer_name(qs, bufname);
    if (b == NULL)
        return 0;

    ShellState *s = qe_get_buffer_mode_data(b, &shell_mode, NULL);
    if (s == NULL || s->pid < 0)
        return 0;

    ShellKillContext *sc = qe_mallocz(ShellKillContext);
    if (!sc)
        return 0;
    sc->s = s;
    sc->e = e;
    sc->b = b;
    sc->callback = cb;
    sc->cmd = qe_strdup(cmd);
    snprintf(buf, sizeof(buf),
             "A %s process is running; kill it? (yes or no) ",
             s->caption ? s->caption : "shell");
    minibuffer_edit(e, NULL, buf, NULL, NULL,
                    shell_kill_process_confirm_cb, sc);
    return 1;
}

static void do_shell_command(EditState *e, const char *cmd)
{
    char curpath[MAX_FILENAME_SIZE];
    QEmacsState *qs = e->qs;
    const char *bufname = "*shell command output*";
    EditBuffer *b;

    get_default_path(e->b, e->offset, curpath, sizeof curpath);

    if (shell_check_active_process(qs, bufname, do_shell_command, e, cmd))
        return;

    /* create new buffer */
    b = qe_new_shell_buffer(qs, NULL, e, bufname, NULL,
                            curpath, cmd, SF_COLOR |
                            SF_REUSE_BUFFER | SF_ERASE_BUFFER);
    if (!b)
        return;

    e = shell_target_window(e, b);
    edit_set_mode(e, &pager_mode);
}

static void do_interactive_shell_command(EditState *e, const char *cmd)
{
    char curpath[MAX_FILENAME_SIZE];
    QEmacsState *qs = e->qs;
    EditBuffer *b;

    get_default_path(e->b, e->offset, curpath, sizeof curpath);

    /* create new buffer */
    b = qe_new_shell_buffer(qs, NULL, e, "*interactive shell command*", "Shell process",
                            curpath, cmd, SF_COLOR | SF_INTERACTIVE);
    if (!b)
        return;

    b->default_mode = &shell_mode;
    switch_to_buffer(e, b);
    shell_mode.mode_init(e, b, 0);
    set_error_offset(b, 0);
    put_status(e, "Press C-o to toggle between shell input and buffer navigation");
}

static void do_compile(EditState *s, const char *cmd)
{
    char curpath[MAX_FILENAME_SIZE];
    QEmacsState *qs = s->qs;
    const char *bufname = "*compilation*";
    EditBuffer *b;

    if (s->flags & (WF_POPUP | WF_MINIBUF))
        return;

    if (qe_save_buffers(s->qs, SBF_EXAMINE))
        return;

    get_default_path(s->b, s->offset, curpath, sizeof curpath);

    if (s->flags & WF_POPLEFT) {
        /* avoid messing with the dired pane */
        s = find_window(s, KEY_RIGHT, s);
        qs->active_window = s;
    }

    if (!cmd || !*cmd)
        cmd = "make";

    if (shell_check_active_process(qs, bufname, do_compile, s, cmd))
        return;

    /* create new buffer */
    b = qe_new_shell_buffer(qs, NULL, s, bufname, "Compilation",
                            curpath, cmd, SF_COLOR |
                            SF_REUSE_BUFFER | SF_ERASE_BUFFER);
    if (!b)
        return;

    b->data_type_name = "compile";
    s = shell_target_window(s, b);
    edit_set_mode(s, &compilation_mode);
    set_error_offset(b, 0);
}

/* Scan a buffer for an error message or a grep location */
static int match_error(EditBuffer *b, int start_offset, ShellError *dest)
{
    char filename[MAX_FILENAME_SIZE];
    char fullpath[MAX_FILENAME_SIZE];
    buf_t fname[1];
    int line_num, col_num, len;
    int offset = start_offset;
    char32_t c;

    /* parse filename:linenum:message */
    /* and:  filename(linenum[, col]) ...: ... */

    /* extract filename */
    buf_init(fname, filename, countof(filename));
    for (;;) {
        c = eb_nextc(b, offset, &offset);
        if (c == '\n' || c == '\t' || c == ' ')
            return 0;
        if (c == ':' || c == '(')
            break;
        buf_putc_utf8(fname, c);
    }

    /* extract line number */
    for (line_num = 0;;) {
        c = eb_nextc(b, offset, &offset);
        if (c == ':' || c == ',' || c == '.' || c == ')')
            break;
        if (!qe_isdigit(c))
            return 0;
        line_num = line_num * 10 + c - '0';
    }
    if (line_num < 1)
        return 0;

    /* extract optional column number */
    col_num = 0;
    if (c == ':' || c == ',' || c == '.') {
        int offset0 = offset;
        char32_t c0 = c;
        for (;;) {
            c = eb_nextc(b, offset, &offset);
            if (c == ' ') continue;
            if (!qe_isdigit(c))
                break;
            col_num = col_num * 10 + c - '0';
        }
        if (col_num == 0) {
            offset = offset0;
            c = c0;
        }
    }
    while (c != ':') {
        if (c == '\n')
            return 0;
        c = eb_nextc(b, offset, &offset);
    }
    if (dest) {
        canonicalize_absolute_buffer_path(b, start_offset,
                                          fullpath, countof(fullpath),
                                          filename);
        if (line_num != dest->line_num
        ||  col_num != dest->col_num
        ||  !strequal(fullpath, dest->filename)) {
            dest->offset = start_offset;
            dest->msg_offset = offset;
            dest->line_num = line_num;
            dest->col_num = col_num;
            pstrcpy(dest->filename, countof(dest->filename), fullpath);
            len = eb_fgets(b, dest->message, countof(dest->message),
                           offset, &offset);
            dest->message[len] = '\0';   /* strip the trailing newline if any */
            return 2;  // new error
        }
    }
    return 1; // has error
}

static void do_next_error(EditState *s, int arg, int dir)
{
    QEmacsState *qs = s->qs;
    EditState *e;
    EditBuffer *b;
    int offset;
    struct stat sb;

    if (s->flags & (WF_POPUP | WF_MINIBUF))
        return;

    if (s->flags & WF_POPLEFT) {
        /* avoid messing with the dired pane */
        s = find_window(s, KEY_RIGHT, s);
        qs->active_window = s;
    }

    if (arg != NO_ARG) {
        /* called with a prefix: set the error source to the current buffer */
        // XXX: has_arg should scan for errors from the next file
        set_error_offset(s->b, s->offset);
    }

    if ((b = qe_find_buffer_name(qs, error_state.buffer)) == NULL) {
        if (s->b->flags & BF_ERROR) {
            b = s->b;
        } else {
            EditBuffer *b1;
            int atime = -1;

            for (b1 = qs->first_buffer; b1 != NULL; b1 = b1->next) {
                if ((b1->flags & BF_ERROR) && b1->atime > atime) {
                    b = b1;
                    atime = b1->atime;
                }
            }
            if (b == NULL && (b = qe_find_buffer_name(qs, "*errors*")) == NULL) {
                put_error(s, "No compilation buffer");
                return;
            }
        }
        set_error_offset(b, -1);
    }

    /* find next/prev error */
    offset = error_state.offset;

    /* CG: should use higher level parsing */
    for (;;) {
        if (dir > 0) {
            offset = eb_next_line(b, offset);
            if (offset >= b->total_size) {
                put_error(s, "No more errors");
                return;
            }
        } else {
            if (offset <= 0) {
                put_error(s, "No previous error");
                return;
            }
            offset = eb_prev_line(b, offset);
        }
        if (match_error(b, offset, &error_state) > 1) {
            // found a new error
            break;
        }
    }

    /* check file and go to the error */
    if (stat(error_state.filename, &sb) < 0) {
        // XXX: should prompt for fullpath as a fallback
        put_error(qs->active_window, "Cannot find error in '%s': %s",
                  error_state.filename, strerror(errno));
        return;
    } else
    if (!S_ISREG(sb.st_mode)) {
        put_error(qs->active_window, "Cannot find error in '%s': %s",
                  error_state.filename, "not a regular file");
        return;
    }

    if ((e = get_next_window(s, WF_POPLEFT, WF_POPLEFT)))
        do_delete_window(e, 0);

    /* find a window showing the error source
       buffer, split a new one if none is found */
    e = eb_find_window(b, NULL);
    if (qs->shell_command_other_window) {
        if (e == NULL)
            e = shell_target_window(s, b);
        /* if there is only one window showing the error source buffer
           then find or split a new window to find the file in */
        if (e == s && !(s = get_next_window(s, WF_MINIBUF | WF_HIDDEN, 0)))
            s = shell_target_window(e, b);
    }

    if (e) {
        /* update error window to show the error at top */
        e->interactive = 0;
        e->offset_top = e->offset = error_state.offset;
        if (qs->shell_buffer_read_only)
            b->flags |= BF_READONLY;
    }

    /* set and update active window */
    qs->active_window = s;
    do_find_file(s, error_state.filename, 0);
    do_goto_line(s, error_state.line_num, error_state.col_num);
    do_center_cursor(s, 1);

#if 0  // conflicts with muscle memory (chqrlie)
    if (!qs->first_transient_key) {
        qe_register_transient_binding(qs, "next-error", "M-n");
        qe_register_transient_binding(qs, "previous-error", "M-p");
    }
#endif
    put_status(s, "=> %s", error_state.message);
}

static int match_digits(const char32_t *buf, int n, char32_t sep) {
    if (n >= 2 && qe_isdigit(buf[0])) {
        int i = 1;
        while (i < n && qe_isdigit(buf[i]))
            i++;
        if (buf[i] == sep) {
            return i + 1;
        }
    }
    return 0;
}

static int match_string(const char32_t *buf, int n, const char *str) {
    int i;
    for (i = 0; i < n && str[i] && buf[i] == (u8)str[i]; i++)
        continue;
    return (str[i] == '\0') ? i : 0;
}

// Try and match diff marks. Accept patch style and default diff style
static int match_diff(const char32_t *buf, int n) {
    int i = 0;

    // Accept lines starting with @@ (patch style location lines)
    if (n > 2 && buf[0] == '@' && buf[1] == '@')
        return 1;

    // Accept --- (diff style block separators)
    if (n == 3 && buf[0] == '-' && buf[1] == '-' && buf[2] == '-')
        return 1;

    // Accept lines matching [0-9]+[adc][0-9]+(,[0-9]+)? (diff style location lines)
    if (!qe_isdigit(buf[i])) return 0;
    while (qe_isdigit(buf[++i]))
        continue;
    if (i == n || (buf[i] != 'a' && buf[i] != 'd' && buf[i] != 'c'))
        return 0;
    if (!qe_isdigit(buf[++i])) return 0;
    while (qe_isdigit(buf[++i]))
        continue;
    if (i == n) return 1;
    if (buf[i] != ',') return 0;
    if (!qe_isdigit(buf[++i])) return 0;
    while (qe_isdigit(buf[++i]))
        continue;
    if (i == n) return 1;

    return 0;
}

static int shell_grab_filename(const char32_t *buf, int n,
                               char *dest, int size, int filter)
{
    int i, j, len = 0;
    for (i = 0; i < n; i++) {
        char32_t c = buf[i];
        if (filter) {
            if (c == '(')
                break;
            if (c == ':' && i > 1)
                break;
            if (c == '-' && i > 1) {
                /* match -[0-9]+- for grep -[ABC] output */
                for (j = i + 1; qe_isdigit(buf[j]); j++)
                    continue;
                if (j > i + 1 && buf[j] == '-')
                    break;
            }
        }
        if (qe_isspace(c) && buf[0] != '#') {
            if (len)
                break;
            continue;
        }
        if (len + 1 < size) {
            dest[len++] = c;
        }
    }
    if (size)
        dest[len] = '\0';
    return i;
}

#define STATE_SHELL_SHIFT  7
#define STATE_SHELL_MODE   0x001F
#define STATE_SHELL_SKIP   0x0020
#define STATE_SHELL_KEEP   0x0040
#define STATE_SHELL_MASK   ((1 << STATE_SHELL_SHIFT) - 1)

static ModeDef *mode_cache[STATE_SHELL_MODE + 1];
static int mode_cache_len = 1;

static int qe_shell_find_mode(QEmacsState *qs, const char *filename) {
    ModeDef *m = qe_find_mode_filename(qs, filename, MODEF_SYNTAX);
    int i;

    for (i = 0; i < mode_cache_len; i++) {
        if (m == mode_cache[i])
            return i;
    }
    if (i == countof(mode_cache))
        return 0;

    mode_cache[i] = m;
    mode_cache_len = i + 1;
    return i;
}

void shell_colorize_line(QEColorizeContext *cp,
                         const char32_t *str, int n,
                         QETermStyle *sbuf, ModeDef *syn)
{
    /* detect match lines for known languages and colorize accordingly */
    char filename[MAX_FILENAME_SIZE];
    ModeDef *m;
    int i, start = 0, stop = n;

    if (qe_isspace(str[0])) {
        /* patch style common lines */
        if (cp->colorize_state & STATE_SHELL_SKIP) {
            start = stop = 1;
        } else {
            /* clang line number prefix */
            for (i = 0; i < 8; i++) {
                if (str[i] != ' ' && !qe_isdigit(str[i]))
                    break;
            }
            if (str[i] == '|' && str[i + 1] == ' ')
                start = stop = i + 2;
        }
    } else {
        /* Detect patches and colorize according to filename */
        if ((cp->colorize_state & STATE_SHELL_KEEP) && match_diff(str, n)) {  /* patch/diff location lines */
            /* reset current multiline state */
            cp->colorize_state &= STATE_SHELL_MASK;
            if (cp->colorize_state & STATE_SHELL_MODE) {
                cp->line_style = QE_STYLE_DIFF_MARK;
                cp->combine_stop = 0;
            }
            return;
        } else
        if (str[0] == '+' || str[0] == '-') {
            if (match_string(str, n, "+++ ") || match_string(str, n, "--- ")) {
                /* extract filename, strip some known prefixes and suffixes */
                const char32_t *p = str + 4;
                const char32_t *q = str + n;
                int pref = match_string(p, q - p, "#<buffer ");
                if (pref && q[-1] == '>') {
                    p += pref;
                    q--;
                }
                if (q[-1] == '>' && qe_isdigit(q[-2])) {
                    q -= 2;
                    while (qe_isdigit(q[-1]))
                        q--;
                    if (q[-1] == '<')
                        q--;
                }
                shell_grab_filename(p, q - p, filename, countof(filename), FALSE);
                cp->colorize_state = qe_shell_find_mode(cp->s->qs, filename);
                cp->colorize_state |= STATE_SHELL_SKIP | STATE_SHELL_KEEP;
                cp->line_style = QE_STYLE_DIFF_HEAD;
                cp->combine_stop = 0;
                return;
            } else {
                if (cp->colorize_state & STATE_SHELL_SKIP) {
                    if (str[0] == '-')
                        cp->line_style = QE_STYLE_DIFF_OLD;
                    else
                        cp->line_style = QE_STYLE_DIFF_NEW;
                    start = 1;
                    stop = 0;
                }
            }
        } else
        if (str[0] == '<' || str[0] == '>') {
            if (str[1] == ' ')
                start = stop = 2;
            else
                return;
        } else
        if (match_string(str, n, "diff ") || match_string(str, n, "Only in ")) {
            // XXX if in colored output, should use cp->line_style = QE_STYLE_DIFF_OLD
            return;
        } else
        if (match_string(str, n, "==> ") && match_string(str + n - 4, 4, " <==")) {
            /* head and tail file marker */
            shell_grab_filename(str + 4, n - 8, filename, countof(filename), FALSE);
            cp->colorize_state = qe_shell_find_mode(cp->s->qs, filename);
            cp->colorize_state |= STATE_SHELL_KEEP;
            cp->line_style = QE_STYLE_DIFF_HEAD;
            cp->combine_stop = 0;
            return;
        } else {
            /* Detect compiler messages and colorize according to filename */
            for (i = 0; i < n;) {
                int w = 0;
                char32_t c = str[i];
                if (qe_isspace(c)) {
                    i++;
                    if (match_string(str + i, n - i, "> ")
                    ||  match_string(str + i, n - i, "$ ")) {
                        /* Detect invocations of file commands */
                        static const char * const commands[] = {
                            "diff ", "head ", "tail ", "cat ", NULL
                        };
                        int k;
                        i += 2;
                        for (k = 0; commands[k]; k++) {
                            if ((w = match_string(str + i, n - i, commands[k])) != 0)
                                break;
                        }
                        if (w) {
                            for (i += w; i < n; i++) {
                                if (qe_isspace(str[i]))
                                    continue;
                                i += shell_grab_filename(str + i, n - i, filename, countof(filename), FALSE);
                                if (*filename == '-')
                                    continue;
                                cp->colorize_state = qe_shell_find_mode(cp->s->qs, filename);
                                if (cp->colorize_state) {
                                    cp->colorize_state |= STATE_SHELL_KEEP;
                                    return;
                                }
                            }
                            return;
                        }
                    }
                } else {
                    int mc;
                    w = shell_grab_filename(str + i, n - i, filename, countof(filename), TRUE);
                    if (i == 0) {
                        char *p = strchr(filename, '@');
                        if (p != NULL && p > filename && p[-1] != ' ') {
                            /* This looks like a prompt: reset the current mode */
                            cp->colorize_state = 0;
                            i += w;
                            continue;
                        }
                    }
                    if (w <= 0) {
                        i++;
                        continue;
                    }
                    i += w;
                    mc = qe_shell_find_mode(cp->s->qs, filename);
                    if (!mc)
                        continue;
                    c = str[i];
                    if (c == '(') {
                        /* this is an old style filename position */
                        i += 1;
                        i += match_digits(str + i, n - i, ')');
                        i += (str[i] == ':');
                        cp->colorize_state = mc;
                        start = i;
                        stop = 0;
                        SET_STYLE(sbuf, 0, i, QE_STYLE_ERROR_LOCATION);
                        break;
                    }
                    /* colorize compiler and grep -[ABC] output */
                    if (c == ':' || (c == '-' && qe_isdigit(str[i + 1]))) {
                        /* this is a compiler message */
                        i += 1;
                        i += match_digits(str + i, n - i, c); /* line number */
                        i += match_digits(str + i, n - i, c); /* optional col number */
                        start = i;
                        cp->colorize_state = mc;
                        stop = 0;
                        SET_STYLE(sbuf, 0, i, QE_STYLE_ERROR_LOCATION);
                        if (match_string(str + i, n - i, " error:")
                        ||  match_string(str + i, n - i, " note:")
                        ||  match_string(str + i, n - i, " warning:")) {
                            /* compiler diagnostic, will colorize subsequent lines */
                            cp->colorize_state |= STATE_SHELL_KEEP;
                            return;
                        }
                        break;
                    }
                }
            }
        }
    }
    if ((cp->colorize_state & STATE_SHELL_MODE)
    &&  (m = mode_cache[cp->colorize_state & STATE_SHELL_MODE]) != NULL) {
        int save_state = cp->colorize_state;
        cp->colorize_state >>= STATE_SHELL_SHIFT;
        cp->partial_file++;
        cp->mode_flags = m->flags;
        cp_colorize_line(cp, str, start, n, sbuf, m);
        cp->partial_file--;
        if (save_state & STATE_SHELL_KEEP) {
            cp->colorize_state <<= STATE_SHELL_SHIFT;
            cp->colorize_state |= save_state & STATE_SHELL_MASK;
        } else {
            cp->colorize_state = 0;
        }
        cp->combine_stop = stop;
        cp->combine_skip = start; // restrict trailing blank colorizer
    } else {
        cp->colorize_state = 0;
    }
}

/* shell mode specific commands */
static const CmdDef shell_commands[] = {
    CMD0( "shell-toggle-input", "C-o, C-c C-o",
          "Toggle between shell input and buffer navigation",
          do_shell_toggle_input)
    /* XXX: should have shell-execute-line on M-RET */
    CMD2( "shell-enter", "RET, LF",
          "Shell buffer RET key",
          do_shell_newline, ES, "*")
    /* CG: should send s->kbs */
    CMD2( "shell-backward-delete-char", "DEL",
          "Shell buffer DEL key",
          do_shell_backspace, ES, "*")
    CMD0( "shell-intr", "C-c C-c",
          "Shell buffer ^C key",
          do_shell_intr)
    CMD2( "shell-delete-char", "C-d, delete",
          "Shell buffer delete char",
          do_shell_delete_char, ES, "*")
    CMD3( "shell-kill-word", "M-d",
          "Shell buffer delete word",
          do_shell_kill_word, ESi, "v", 1)
    CMD3( "shell-backward-kill-word", "M-DEL, M-C-h",
          "Shell buffer delete word backward",
          do_shell_kill_word, ESi, "v", -1)
    CMD1( "shell-previous", "M-p",
          "Shell buffer previous command",
          shell_previous_next, -1)
    CMD1( "shell-next", "M-n",
          "Shell buffer next command",
          shell_previous_next, 1)
    CMD0( "shell-exchange-point-and-mark", "C-x C-x",
          "Shell buffer ^X^X",
          shell_exchange_point_and_mark)
    CMD2( "shell-tabulate", "TAB",
          "Shell buffer TAB key",
          do_shell_tabulate, ES, "*")
    CMD1( "shell-refresh", "C-l",
          "Refresh shell buffer window and update terminal size",
         do_shell_refresh, SR_UPDATE_SIZE | SR_REFRESH)
    CMD1( "shell-search-backward", "C-r",
          "Shell buffer ^R key",
          do_shell_search, -1)
    CMD1( "shell-search-forward", "C-s",
          "Shell buffer ^S key",
          do_shell_search, 1)
    CMD2( "shell-kill-line", "C-k",
          "Shell buffer kill line",
          do_shell_kill_line, ESi, "P")
    CMD2( "shell-kill-beginning-of-line", "M-k",
          "Shell buffer kill beginning of line",
          do_shell_kill_beginning_of_line, ESi, "P")
    CMD2( "shell-yank", "C-y",
          "Shell buffer yank",
          do_shell_yank, ES, "*")
    CMD3( "shell-capitalize-word", "M-c",
          "Shell buffer capitalize word",
          do_shell_changecase_word, ESi, "*" "v", 2)
    CMD3( "shell-downcase-word", "M-l",
          "Shell buffer downcase word",
          do_shell_changecase_word, ESi, "*" "v", -1)
    CMD3( "shell-upcase-word", "M-u",
          "Shell buffer upcase",
          do_shell_changecase_word, ESi, "*" "v", 1)
    CMD3( "shell-transpose-chars", "C-t",
          "Shell buffer ^T key",
          do_shell_transpose, ESi, "*" "v", CMD_TRANSPOSE_CHARS)
    CMD3( "shell-transpose-words", "M-t",
          "Shell buffer transpose words",
          do_shell_transpose, ESi, "*" "v", CMD_TRANSPOSE_WORDS)
};

/* shell global commands */
static const CmdDef shell_global_commands[] = {
    CMD2( "shell", "C-x RET RET, C-x LF LF",
          "Start a shell buffer or move to the last shell buffer used",
          do_shell, ESi, "#" "p")
    CMD2( "shell-command", "M-!",
          "Run a shell command and display a new buffer with its collected output",
          do_shell_command, ESs,
          "s{Shell command: }|shell-command|")
    CMD2( "interactive-shell-command", "",
          "Run a shell command interactively and display a new buffer with its collected output",
          do_interactive_shell_command, ESs,
          "#" "s{Interactive shell command: }|interactive-shell-command|")
    CMD2( "ssh", "",
          "Start a shell buffer with a new remote shell connection",
          do_ssh, ESs,
          "#" "s{Open connection to (host or user@host: }|ssh|")
    CMD2( "compile", "C-x C-e",
          "Run a compiler command and display a new buffer with its collected output",
          do_compile, ESs,
          "#" "s{Compile command: }|compile|")
    CMD2( "make", "C-x m",
          "Run make and display a new buffer with its collected output",
          do_compile, ESs,
          "#" "@{make}")
    CMD2( "man", "",
          "Run man for a command and display a new buffer with its collected output",
          do_man, ESs,
          "#" "s{Show man page for: }|man|")
    CMD3( "next-error", "C-x C-n, C-x `, M-g n, M-g M-n",
          "Move to the next error from the last shell command output",
          do_next_error, ESii, "#" "P" "v", +1)
    CMD3( "previous-error", "C-x C-p, M-g p, M-g M-p",
          "Move to the previous error from the last shell command output",
          do_next_error, ESii, "#" "P" "v", -1)
    CMD2( "diff-buffer-with-file", "C-c C-d, C-c =",
          "Show differences between the buffer in the current window and its file",
          do_diff_buffer_with_file, ES, "#")
};

static int shell_mode_probe(ModeDef *mode, ModeProbeData *p)
{
    ShellState *s = qe_get_buffer_mode_data(p->b, &shell_mode, NULL);

    if (s && s->pid >= 0)
        return 100;

    return 0;
}

static int shell_mode_init(EditState *e, EditBuffer *b, int flags)
{
    if (e) {
        ShellState *s;

        if (!(s = shell_get_state(e, 1)))
            return -1;

        e->b->tab_width = 8;
        e->wrap = WRAP_TERM;
        e->wrap_cols = s->cols;
        if ((s->shell_flags & SF_INTERACTIVE) && !s->grab_keys) {
            e->interactive = 1;
            e->b->flags &= ~BF_READONLY;
        }
    }
    return 0;
}

static int pager_mode_init(EditState *e, EditBuffer *b, int flags)
{
    if (e) {
        e->b->tab_width = 8;
        e->wrap = pager_mode.default_wrap;
    }
    if (b) {
        b->modified = 0;
        b->flags |= BF_READONLY;
    }
    return 0;
}

static int compilation_mode_init(EditState *e, EditBuffer *b, int flags)
{
    if (e) {
        e->b->tab_width = 8;
        e->wrap = compilation_mode.default_wrap;
    }
    if (b) {
        b->modified = 0;
        b->flags |= BF_ERROR | BF_READONLY;
    }
    return 0;
}

static void do_pager_abort(EditState *e)
{
    EditBuffer *b1;

    if (e->mode != &pager_mode) {
        put_error(e, "Not a %s buffer", pager_mode.name);
        return;
    }

    b1 = qe_check_buffer(e->qs, &e->last_buffer);
    if (b1 != NULL) {
        switch_to_buffer(e, NULL);
        e->last_buffer = NULL;
        switch_to_buffer(e, b1);
    } else {
        do_delete_window(e, 0);
    }
}

static const CmdDef pager_commands[] = {
    CMD0( "pager-abort", "q",
          "Quit pager mode",
          do_pager_abort)
};

/* additional mode specific bindings */
static const char * const pager_bindings[] = {
    "DEL", "scroll-down",
    "SPC", "scroll-up",
    "/", "search-forward",
    NULL
};

static void do_kill_compilation(EditState *e)
{
    ShellState *s;
    EditBuffer *b = e->b;
    QEmacsState *qs = e->qs;

    if (e->mode != &compilation_mode) {
        if (!(b = qe_find_buffer_name(qs, "*compilation*"))) {
            put_error(e, "No compilation buffer");
            return;
        }
    }

    if ((s = qe_get_buffer_mode_data(b, &shell_mode, NULL)) && s->pid > 0)
        kill(s->pid, SIGKILL);
    else
        put_error(e, "Compilation process is not running");
}

static void do_goto_error(EditState *s)
{
    int offset;

    if (!(s->b->flags & BF_ERROR)) {
        put_error(s, "Not an error source buffer");
        return;
    }
    offset = eb_goto_bol(s->b, s->offset);
    if (!match_error(s->b, offset, NULL)) {
        put_error(s, "No error here");
    } else {
        set_error_offset(s->b, offset);
        do_next_error(s, NO_ARG, +1);
    }
}

static const CmdDef compilation_commands[] = {
    CMD0( "goto-error", "RET, LF", // XXX: mouse click ?
          "Move to the error matching the current error message at point",
          do_goto_error)
    CMD0( "kill-compilation", "C-c C-k",
          "kill compilation process",
          do_kill_compilation)
};

static const char * const compilation_bindings[] = {
    "n, M-n", "next-error",
    "p, M-p", "previous-error",
    "DEL", "scroll-down",
    "SPC", "scroll-up",
    "/", "search-forward",
    NULL
};

static int shell_init(QEmacsState *qs)
{
    /* populate and register shell mode and commands */
    // XXX: remove this mess: should just inherit with fallback
    memcpy(&shell_mode, &text_mode, offsetof(ModeDef, first_key));
    shell_mode.name = "shell";
    shell_mode.flags |= MODEF_NO_TRAILING_BLANKS;
    shell_mode.mode_probe = shell_mode_probe;
    shell_mode.colorize_func = shell_colorize_line;
    shell_mode.buffer_instance_size = sizeof(ShellState);
    shell_mode.mode_init = shell_mode_init;
    shell_mode.mode_free = shell_mode_free;
    shell_mode.display_hook = shell_display_hook;
    shell_mode.move_left_right = shell_move_left_right;
    shell_mode.move_word_left_right = shell_move_word_left_right;
    shell_mode.move_up_down = shell_move_up_down;
    shell_mode.scroll_up_down = shell_scroll_up_down;
    shell_mode.move_bol = shell_move_bol;
    shell_mode.move_eol = shell_move_eol;
    shell_mode.move_bof = shell_move_bof;
    shell_mode.move_eof = shell_move_eof;
    shell_mode.write_char = shell_write_char;
    shell_mode.delete_bytes = shell_delete_bytes;
    shell_mode.get_default_path = shell_get_default_path;

    qe_register_mode(qs, &shell_mode, MODEF_NOCMD | MODEF_VIEW);
    qe_register_commands(qs, &shell_mode, shell_commands, countof(shell_commands));
    qe_register_commands(qs, NULL, shell_global_commands, countof(shell_global_commands));

    /* populate and register pager mode and commands */
    // XXX: remove this mess: should just inherit with fallback
    memcpy(&pager_mode, &text_mode, offsetof(ModeDef, first_key));
    pager_mode.name = "pager";
    pager_mode.flags |= MODEF_NO_TRAILING_BLANKS;
    pager_mode.mode_probe = NULL;
    pager_mode.mode_init = pager_mode_init;
    pager_mode.bindings = pager_bindings;
    pager_mode.colorize_func = shell_colorize_line;
    pager_mode.default_wrap = WRAP_LINE;

    qe_register_mode(qs, &pager_mode, MODEF_NOCMD | MODEF_VIEW);
    qe_register_commands(qs, &pager_mode, pager_commands, countof(pager_commands));

    /* populate and register compilation mode and commands */
    // XXX: remove this mess: should just inherit with fallback
    memcpy(&compilation_mode, &text_mode, offsetof(ModeDef, first_key));
    compilation_mode.name = "compilation";
    compilation_mode.flags |= MODEF_NO_TRAILING_BLANKS;
    compilation_mode.mode_probe = NULL;
    compilation_mode.mode_init = compilation_mode_init;
    compilation_mode.bindings = compilation_bindings;
    compilation_mode.colorize_func = shell_colorize_line;
    compilation_mode.default_wrap = WRAP_LINE;

    qe_register_mode(qs, &compilation_mode, MODEF_NOCMD | MODEF_VIEW);
    qe_register_commands(qs, &compilation_mode, compilation_commands, countof(compilation_commands));

    return 0;
}

qe_module_init(shell_init);
