diff --git putty-0.81/putty.h putty-url-0.81/putty.h
index e4444c0..1d14425 100644
--- putty-0.81/putty.h
+++ putty-url-0.81/putty.h
@@ -259,6 +259,22 @@ extern const int colour_indices_oscp_to_osc4[OSCP_NCOLOURS];
 #define ATTR_DEFBG   (OSC4_COLOUR_bg << ATTR_BGSHIFT)
 #define ATTR_DEFAULT (ATTR_DEFFG | ATTR_DEFBG)
 
+/*
+ * HACK: PuttyTray / Nutty
+ * Hyperlink stuff: define
+ */
+#define CHAR_MASK    0x000000FFUL
+
+/*
+ * HACK: PuttyTray / Nutty
+ * Hyperlink stuff: Underline settings
+ */
+enum {
+	URLHACK_UNDERLINE_ALWAYS,
+	URLHACK_UNDERLINE_HOVER,
+	URLHACK_UNDERLINE_NEVER
+};
+
 struct sesslist {
     int nsessions;
     const char **sessions;
@@ -2052,6 +2068,13 @@ NORETURN void cleanup_exit(int);
     X(INT, NONE, shadowboldoffset) /* in pixels */ \
     X(BOOL, NONE, crhaslf) \
     X(STR, NONE, winclass) \
+    /* HACK: PuTTY-url settings */ \
+    X(BOOL, NONE, url_ctrl_click) \
+    X(INT, NONE, url_underline) \
+    X(BOOL, NONE, url_defbrowser) \
+    X(BOOL, NONE, url_defregex) \
+    X(FILENAME, NONE, url_browser) \
+    X(STR, NONE, url_regex) \
     /* end of list */
 
 /* Now define the actual enum of option keywords using that macro. */
diff --git putty-url-0.81/putty.h.orig putty-url-0.81/putty.h.orig
new file mode 100644
index 0000000..6c5ed99
--- /dev/null
+++ putty-url-0.81/putty.h.orig
@@ -0,0 +1,2949 @@
+#ifndef PUTTY_PUTTY_H
+#define PUTTY_PUTTY_H
+
+#include <stddef.h>                    /* for wchar_t */
+#include <limits.h>                    /* for INT_MAX */
+
+#include "defs.h"
+#include "platform.h"
+#include "network.h"
+#include "misc.h"
+#include "marshal.h"
+
+/*
+ * We express various time intervals in unsigned long minutes, but may need to
+ * clip some values so that the resulting number of ticks does not overflow an
+ * integer value.
+ */
+#define MAX_TICK_MINS   (INT_MAX / (60 * TICKSPERSEC))
+
+/*
+ * Fingerprints of the current and previous PGP master keys, to
+ * establish a trust path between an executable and other files.
+ */
+#define PGP_MASTER_KEY_YEAR "2023"
+#define PGP_MASTER_KEY_DETAILS "RSA, 4096-bit"
+#define PGP_MASTER_KEY_FP                                  \
+    "28D4 7C46 55E7 65A6 D827  AC66 B15D 9EFC 216B 06A1"
+#define PGP_PREV_MASTER_KEY_YEAR "2021"
+#define PGP_PREV_MASTER_KEY_DETAILS "RSA, 3072-bit"
+#define PGP_PREV_MASTER_KEY_FP                                  \
+    "A872 D42F 1660 890F 0E05  223E DD43 55EA AC11 19DE"
+
+/*
+ * Definitions of three separate indexing schemes for colour palette
+ * entries.
+ *
+ * Why three? Because history, sorry.
+ *
+ * Two of the colour indexings are used in escape sequences. The
+ * Linux-console style OSC P sequences for setting the palette use an
+ * indexing in which the eight standard ANSI SGR colours come first,
+ * then their bold versions, and then six extra colours for default
+ * fg/bg and the terminal cursor. And the xterm OSC 4 sequences for
+ * querying the palette use a related indexing in which the six extra
+ * colours are pushed up to indices 256 and onwards, with the previous
+ * 16 being the first part of the xterm 256-colour space, and 240
+ * additional terminal-accessible colours inserted in the middle.
+ *
+ * The third indexing is the order that the colours appear in the
+ * PuTTY configuration panel, and also the order in which they're
+ * described in the saved session files. This order specifies the same
+ * set of colours as the OSC P encoding, but in a different order,
+ * with the default fg/bg colours (which users are most likely to want
+ * to reconfigure) at the start, and the ANSI SGR colours coming
+ * later.
+ *
+ * So all three indices really are needed, because all three appear in
+ * protocols or file formats outside the PuTTY binary. (Changing the
+ * saved-session encoding would have a backwards-compatibility impact;
+ * also, if we ever do, it would be better to replace the numeric
+ * indices with descriptive keywords.)
+ *
+ * Since the OSC 4 encoding contains the full set of colours used in
+ * the terminal display, that's the encoding used by front ends to
+ * store any actual data associated with their palette entries. So the
+ * TermWin palette_set and palette_get_overrides methods use that
+ * encoding, and so does the bitwise encoding of attribute words used
+ * in terminal redraw operations.
+ *
+ * The Conf encoding, of course, is used by config.c and settings.c.
+ *
+ * The aim is that those two sections of the code should never need to
+ * come directly into contact, and the only module that should have to
+ * deal directly with the mapping between these colour encodings - or
+ * to deal _at all_ with the intermediate OSC P encoding - is
+ * terminal.c itself.
+ */
+
+#define CONF_NCOLOURS 22               /* 16 + 6 special ones */
+#define OSCP_NCOLOURS 22               /* same as CONF, but different order */
+#define OSC4_NCOLOURS 262              /* 256 + the same 6 special ones */
+
+/* The list macro for the conf colours also gives the textual names
+ * used in the GUI configurer */
+#define CONF_COLOUR_LIST(X)                     \
+    X(fg, "Default Foreground")                 \
+    X(fg_bold, "Default Bold Foreground")       \
+    X(bg, "Default Background")                 \
+    X(bg_bold, "Default Bold Background")       \
+    X(cursor_fg, "Cursor Text")                 \
+    X(cursor_bg, "Cursor Colour")               \
+    X(black, "ANSI Black")                      \
+    X(black_bold, "ANSI Black Bold")            \
+    X(red, "ANSI Red")                          \
+    X(red_bold, "ANSI Red Bold")                \
+    X(green, "ANSI Green")                      \
+    X(green_bold, "ANSI Green Bold")            \
+    X(yellow, "ANSI Yellow")                    \
+    X(yellow_bold, "ANSI Yellow Bold")          \
+    X(blue, "ANSI Blue")                        \
+    X(blue_bold, "ANSI Blue Bold")              \
+    X(magenta, "ANSI Magenta")                  \
+    X(magenta_bold, "ANSI Magenta Bold")        \
+    X(cyan, "ANSI Cyan")                        \
+    X(cyan_bold, "ANSI Cyan Bold")              \
+    X(white, "ANSI White")                      \
+    X(white_bold, "ANSI White Bold")            \
+    /* end of list */
+
+#define OSCP_COLOUR_LIST(X)                     \
+    X(black)                                    \
+    X(red)                                      \
+    X(green)                                    \
+    X(yellow)                                   \
+    X(blue)                                     \
+    X(magenta)                                  \
+    X(cyan)                                     \
+    X(white)                                    \
+    X(black_bold)                               \
+    X(red_bold)                                 \
+    X(green_bold)                               \
+    X(yellow_bold)                              \
+    X(blue_bold)                                \
+    X(magenta_bold)                             \
+    X(cyan_bold)                                \
+    X(white_bold)                               \
+    /*
+     * In the OSC 4 indexing, this is where the extra 240 colours go.
+     * They consist of:
+     *
+     *  - 216 colours forming a 6x6x6 cube, with R the most
+     *    significant colour and G the least. In other words, these
+     *    occupy the space of indices 16 <= i < 232, with each
+     *    individual colour found as i = 16 + 36*r + 6*g + b, for all
+     *    0 <= r,g,b <= 5.
+     *
+     *  - The remaining indices, 232 <= i < 256, consist of a uniform
+     *    series of grey shades running between black and white (but
+     *    not including either, since actual black and white are
+     *    already provided in the previous colour cube).
+     *
+     * After that, we have the remaining 6 special colours:
+     */                                         \
+    X(fg)                                       \
+    X(fg_bold)                                  \
+    X(bg)                                       \
+    X(bg_bold)                                  \
+    X(cursor_fg)                                \
+    X(cursor_bg)                                \
+    /* end of list */
+
+/* Enumerations of the colour lists. These are available everywhere in
+ * the code. The OSC P encoding shouldn't be used outside terminal.c,
+ * but the easiest way to define the OSC 4 enum is to have the OSC P
+ * one available to compute with. */
+enum {
+    #define ENUM_DECL(id,name) CONF_COLOUR_##id,
+    CONF_COLOUR_LIST(ENUM_DECL)
+    #undef ENUM_DECL
+};
+enum {
+    #define ENUM_DECL(id) OSCP_COLOUR_##id,
+    OSCP_COLOUR_LIST(ENUM_DECL)
+    #undef ENUM_DECL
+};
+enum {
+    #define ENUM_DECL(id) OSC4_COLOUR_##id = \
+        OSCP_COLOUR_##id + (OSCP_COLOUR_##id >= 16 ? 240 : 0),
+    OSCP_COLOUR_LIST(ENUM_DECL)
+    #undef ENUM_DECL
+};
+
+/* Mapping tables defined in terminal.c */
+extern const int colour_indices_conf_to_oscp[CONF_NCOLOURS];
+extern const int colour_indices_conf_to_osc4[CONF_NCOLOURS];
+extern const int colour_indices_oscp_to_osc4[OSCP_NCOLOURS];
+
+/* Three attribute types:
+ * The ATTRs (normal attributes) are stored with the characters in
+ * the main display arrays
+ *
+ * The TATTRs (temporary attributes) are generated on the fly, they
+ * can overlap with characters but not with normal attributes.
+ *
+ * The LATTRs (line attributes) are an entirely disjoint space of
+ * flags.
+ *
+ * The DATTRs (display attributes) are internal to terminal.c (but
+ * defined here because their values have to match the others
+ * here); they reuse the TATTR_* space but are always masked off
+ * before sending to the front end.
+ *
+ * ATTR_INVALID is an illegal colour combination.
+ */
+
+#define TATTR_ACTCURS       0x40000000UL      /* active cursor (block) */
+#define TATTR_PASCURS       0x20000000UL      /* passive cursor (box) */
+#define TATTR_RIGHTCURS     0x10000000UL      /* cursor-on-RHS */
+#define TATTR_COMBINING     0x80000000UL      /* combining characters */
+
+#define DATTR_STARTRUN      0x80000000UL   /* start of redraw run */
+
+#define TDATTR_MASK         0xF0000000UL
+#define TATTR_MASK (TDATTR_MASK)
+#define DATTR_MASK (TDATTR_MASK)
+
+#define LATTR_NORM   0x00000000UL
+#define LATTR_WIDE   0x00000001UL
+#define LATTR_TOP    0x00000002UL
+#define LATTR_BOT    0x00000003UL
+#define LATTR_MODE   0x00000003UL
+#define LATTR_WRAPPED 0x00000010UL     /* this line wraps to next */
+#define LATTR_WRAPPED2 0x00000020UL    /* with WRAPPED: CJK wide character
+                                          wrapped to next line, so last
+                                          single-width cell is empty */
+
+#define ATTR_INVALID 0x03FFFFU
+
+/* Use the DC00 page for direct to font. */
+#define CSET_OEMCP   0x0000DC00UL      /* OEM Codepage DTF */
+#define CSET_ACP     0x0000DD00UL      /* Ansi Codepage DTF */
+
+/* These are internal use overlapping with the UTF-16 surrogates */
+#define CSET_ASCII   0x0000D800UL      /* normal ASCII charset ESC ( B */
+#define CSET_LINEDRW 0x0000D900UL      /* line drawing charset ESC ( 0 */
+#define CSET_SCOACS  0x0000DA00UL      /* SCO Alternate charset */
+#define CSET_GBCHR   0x0000DB00UL      /* UK variant   charset ESC ( A */
+#define CSET_MASK    0xFFFFFF00UL      /* Character set mask */
+
+#define DIRECT_CHAR(c) ((c&0xFFFFFC00)==0xD800)
+#define DIRECT_FONT(c) ((c&0xFFFFFE00)==0xDC00)
+
+#define UCSERR       (CSET_LINEDRW|'a') /* UCS Format error character. */
+/*
+ * UCSWIDE is a special value used in the terminal data to signify
+ * the character cell containing the right-hand half of a CJK wide
+ * character. We use 0xDFFF because it's part of the surrogate
+ * range and hence won't be used for anything else (it's impossible
+ * to input it via UTF-8 because our UTF-8 decoder correctly
+ * rejects surrogates).
+ */
+#define UCSWIDE      0xDFFF
+
+#define ATTR_NARROW  0x0800000U
+#define ATTR_WIDE    0x0400000U
+#define ATTR_BOLD    0x0040000U
+#define ATTR_UNDER   0x0080000U
+#define ATTR_REVERSE 0x0100000U
+#define ATTR_BLINK   0x0200000U
+#define ATTR_FGMASK  0x00001FFU /* stores a colour in OSC 4 indexing */
+#define ATTR_BGMASK  0x003FE00U /* stores a colour in OSC 4 indexing */
+#define ATTR_COLOURS 0x003FFFFU
+#define ATTR_DIM     0x1000000U
+#define ATTR_STRIKE  0x2000000U
+#define ATTR_FGSHIFT 0
+#define ATTR_BGSHIFT 9
+
+#define ATTR_DEFFG   (OSC4_COLOUR_fg << ATTR_FGSHIFT)
+#define ATTR_DEFBG   (OSC4_COLOUR_bg << ATTR_BGSHIFT)
+#define ATTR_DEFAULT (ATTR_DEFFG | ATTR_DEFBG)
+
+struct sesslist {
+    int nsessions;
+    const char **sessions;
+    char *buffer;                      /* so memory can be freed later */
+};
+
+struct unicode_data {
+    bool dbcs_screenfont;
+    int font_codepage;
+    int line_codepage;
+    wchar_t unitab_scoacs[256];
+    wchar_t unitab_line[256];
+    wchar_t unitab_font[256];
+    wchar_t unitab_xterm[256];
+    wchar_t unitab_oemcp[256];
+    unsigned char unitab_ctrl[256];
+};
+
+#define LGXF_OVR  1                    /* existing logfile overwrite */
+#define LGXF_APN  0                    /* existing logfile append */
+#define LGXF_ASK -1                    /* existing logfile ask */
+#define LGTYP_NONE  0                  /* logmode: no logging */
+#define LGTYP_ASCII 1                  /* logmode: pure ascii */
+#define LGTYP_DEBUG 2                  /* logmode: all chars of traffic */
+#define LGTYP_PACKETS 3                /* logmode: SSH data packets */
+#define LGTYP_SSHRAW 4                 /* logmode: SSH raw data */
+
+/*
+ * Enumeration of 'special commands' that can be sent during a
+ * session, separately from the byte stream of ordinary session data.
+ */
+typedef enum {
+    /*
+     * Commands that are generally useful in multiple backends.
+     */
+    SS_BRK,    /* serial-line break */
+    SS_EOF,    /* end-of-file on session input */
+    SS_NOP,    /* transmit data with no effect */
+    SS_PING,   /* try to keep the session alive (probably, but not
+                * necessarily, implemented as SS_NOP) */
+
+    /*
+     * Commands specific to Telnet.
+     */
+    SS_AYT,    /* Are You There */
+    SS_SYNCH,  /* Synch */
+    SS_EC,     /* Erase Character */
+    SS_EL,     /* Erase Line */
+    SS_GA,     /* Go Ahead */
+    SS_ABORT,  /* Abort Process */
+    SS_AO,     /* Abort Output */
+    SS_IP,     /* Interrupt Process */
+    SS_SUSP,   /* Suspend Process */
+    SS_EOR,    /* End Of Record */
+    SS_EOL,    /* Telnet end-of-line sequence (CRLF, as opposed to CR
+                * NUL that escapes a literal CR) */
+
+    /*
+     * Commands specific to SSH.
+     */
+    SS_REKEY,  /* trigger an immediate repeat key exchange */
+    SS_XCERT,  /* cross-certify another host key ('arg' indicates which) */
+
+    /*
+     * Send a POSIX-style signal. (Useful in SSH and also pterm.)
+     *
+     * We use the master list in ssh/signal-list.h to define these enum
+     * values, which will come out looking like names of the form
+     * SS_SIGABRT, SS_SIGINT etc.
+     */
+    #define SIGNAL_MAIN(name, text) SS_SIG ## name,
+    #define SIGNAL_SUB(name) SS_SIG ## name,
+    #include "ssh/signal-list.h"
+    #undef SIGNAL_MAIN
+    #undef SIGNAL_SUB
+
+    /*
+     * These aren't really special commands, but they appear in the
+     * enumeration because the list returned from
+     * backend_get_specials() will use them to specify the structure
+     * of the GUI specials menu.
+     */
+    SS_SEP,         /* Separator */
+    SS_SUBMENU,     /* Start a new submenu with specified name */
+    SS_EXITMENU,    /* Exit current submenu, or end of entire specials list */
+} SessionSpecialCode;
+
+/*
+ * The structure type returned from backend_get_specials.
+ */
+struct SessionSpecial {
+    const char *name;
+    SessionSpecialCode code;
+    int arg;
+};
+
+/* Needed by both ssh/channel.h and ssh/ppl.h */
+typedef void (*add_special_fn_t)(
+    void *ctx, const char *text, SessionSpecialCode code, int arg);
+
+typedef enum {
+    MBT_NOTHING,
+    MBT_LEFT, MBT_MIDDLE, MBT_RIGHT,   /* `raw' button designations */
+    MBT_SELECT, MBT_EXTEND, MBT_PASTE, /* `cooked' button designations */
+    MBT_WHEEL_UP, MBT_WHEEL_DOWN,      /* vertical mouse wheel */
+    MBT_WHEEL_LEFT, MBT_WHEEL_RIGHT    /* horizontal mouse wheel */
+} Mouse_Button;
+
+typedef enum {
+    MA_NOTHING, MA_CLICK, MA_2CLK, MA_3CLK, MA_DRAG, MA_RELEASE, MA_MOVE
+} Mouse_Action;
+
+/* Keyboard modifiers -- keys the user is actually holding down */
+
+#define PKM_SHIFT       0x01
+#define PKM_CONTROL     0x02
+#define PKM_META        0x04
+#define PKM_ALT         0x08
+
+/* Keyboard flags that aren't really modifiers */
+#define PKF_CAPSLOCK    0x10
+#define PKF_NUMLOCK     0x20
+#define PKF_REPEAT      0x40
+
+/* Stand-alone keysyms for function keys */
+
+typedef enum {
+    PK_NULL,            /* No symbol for this key */
+    /* Main keypad keys */
+    PK_ESCAPE, PK_TAB, PK_BACKSPACE, PK_RETURN, PK_COMPOSE,
+    /* Editing keys */
+    PK_HOME, PK_INSERT, PK_DELETE, PK_END, PK_PAGEUP, PK_PAGEDOWN,
+    /* Cursor keys */
+    PK_UP, PK_DOWN, PK_RIGHT, PK_LEFT, PK_REST,
+    /* Numeric keypad */                        /* Real one looks like: */
+    PK_PF1, PK_PF2, PK_PF3, PK_PF4,             /* PF1 PF2 PF3 PF4 */
+    PK_KPCOMMA, PK_KPMINUS, PK_KPDECIMAL,       /*  7   8   9   -  */
+    PK_KP0, PK_KP1, PK_KP2, PK_KP3, PK_KP4,     /*  4   5   6   ,  */
+    PK_KP5, PK_KP6, PK_KP7, PK_KP8, PK_KP9,     /*  1   2   3  en- */
+    PK_KPBIGPLUS, PK_KPENTER,                   /*    0     .  ter */
+    /* Top row */
+    PK_F1,  PK_F2,  PK_F3,  PK_F4,  PK_F5,
+    PK_F6,  PK_F7,  PK_F8,  PK_F9,  PK_F10,
+    PK_F11, PK_F12, PK_F13, PK_F14, PK_F15,
+    PK_F16, PK_F17, PK_F18, PK_F19, PK_F20,
+    PK_PAUSE
+} Key_Sym;
+
+#define PK_ISEDITING(k) ((k) >= PK_HOME && (k) <= PK_PAGEDOWN)
+#define PK_ISCURSOR(k)  ((k) >= PK_UP && (k) <= PK_REST)
+#define PK_ISKEYPAD(k)  ((k) >= PK_PF1 && (k) <= PK_KPENTER)
+#define PK_ISFKEY(k)    ((k) >= PK_F1 && (k) <= PK_F20)
+
+enum {
+    VT_XWINDOWS, VT_OEMANSI, VT_OEMONLY, VT_POORMAN, VT_UNICODE
+};
+
+enum {
+    /*
+     * SSH-2 key exchange algorithms
+     */
+    KEX_WARN,
+    KEX_DHGROUP1,
+    KEX_DHGROUP14,
+    KEX_DHGROUP15,
+    KEX_DHGROUP16,
+    KEX_DHGROUP17,
+    KEX_DHGROUP18,
+    KEX_DHGEX,
+    KEX_RSA,
+    KEX_ECDH,
+    KEX_NTRU_HYBRID,
+    KEX_MAX
+};
+
+enum {
+    /*
+     * SSH-2 host key algorithms
+     */
+    HK_WARN,
+    HK_RSA,
+    HK_DSA,
+    HK_ECDSA,
+    HK_ED25519,
+    HK_ED448,
+    HK_MAX
+};
+
+enum {
+    /*
+     * SSH ciphers (both SSH-1 and SSH-2)
+     */
+    CIPHER_WARN,                       /* pseudo 'cipher' */
+    CIPHER_3DES,
+    CIPHER_BLOWFISH,
+    CIPHER_AES,                        /* (SSH-2 only) */
+    CIPHER_DES,
+    CIPHER_ARCFOUR,
+    CIPHER_CHACHA20,
+    CIPHER_AESGCM,
+    CIPHER_MAX                         /* no. ciphers (inc warn) */
+};
+
+enum TriState {
+    /*
+     * Several different bits of the PuTTY configuration seem to be
+     * three-way settings whose values are `always yes', `always
+     * no', and `decide by some more complex automated means'. This
+     * is true of line discipline options (local echo and line
+     * editing), proxy DNS, proxy terminal logging, Close On Exit, and
+     * SSH server bug workarounds. Accordingly I supply a single enum
+     * here to deal with them all.
+     */
+    FORCE_ON, FORCE_OFF, AUTO
+};
+
+enum {
+    /*
+     * Proxy types.
+     */
+    PROXY_NONE, PROXY_SOCKS4, PROXY_SOCKS5,
+    PROXY_HTTP, PROXY_TELNET, PROXY_CMD, PROXY_SSH_TCPIP,
+    PROXY_SSH_EXEC, PROXY_SSH_SUBSYSTEM,
+    PROXY_FUZZ
+};
+
+enum {
+    /*
+     * Line discipline options which the backend might try to control.
+     */
+    LD_EDIT,                           /* local line editing */
+    LD_ECHO,                           /* local echo */
+    LD_N_OPTIONS
+};
+
+enum {
+    /* Actions on remote window title query */
+    TITLE_NONE, TITLE_EMPTY, TITLE_REAL
+};
+
+enum {
+    /* SUPDUP character set options */
+    SUPDUP_CHARSET_ASCII, SUPDUP_CHARSET_ITS, SUPDUP_CHARSET_WAITS
+};
+
+enum {
+    /* Protocol back ends. (CONF_protocol) */
+    PROT_RAW, PROT_TELNET, PROT_RLOGIN, PROT_SSH, PROT_SSHCONN,
+    /* PROT_SERIAL is supported on a subset of platforms, but it doesn't
+     * hurt to define it globally. */
+    PROT_SERIAL,
+    /* PROT_SUPDUP is the historical RFC 734 protocol. */
+    PROT_SUPDUP,
+    PROTOCOL_LIMIT, /* upper bound on number of protocols */
+};
+
+enum {
+    /* Bell settings (CONF_beep) */
+    BELL_DISABLED, BELL_DEFAULT, BELL_VISUAL, BELL_WAVEFILE, BELL_PCSPEAKER
+};
+
+enum {
+    /* Taskbar flashing indication on bell (CONF_beep_ind) */
+    B_IND_DISABLED, B_IND_FLASH, B_IND_STEADY
+};
+
+enum {
+    /* Resize actions (CONF_resize_action) */
+    RESIZE_TERM, RESIZE_DISABLED, RESIZE_FONT, RESIZE_EITHER
+};
+
+enum {
+    /* Function key types (CONF_funky_type) */
+    FUNKY_TILDE,
+    FUNKY_LINUX,
+    FUNKY_XTERM,
+    FUNKY_VT400,
+    FUNKY_VT100P,
+    FUNKY_SCO,
+    FUNKY_XTERM_216
+};
+
+enum {
+    /* Shifted arrow key types (CONF_sharrow_type) */
+    SHARROW_APPLICATION,  /* Ctrl flips between ESC O A and ESC [ A */
+    SHARROW_BITMAP        /* ESC [ 1 ; n A, where n = 1 + bitmap of CAS */
+};
+
+enum {
+    FQ_DEFAULT, FQ_ANTIALIASED, FQ_NONANTIALIASED, FQ_CLEARTYPE
+};
+
+enum {
+    SER_PAR_NONE, SER_PAR_ODD, SER_PAR_EVEN, SER_PAR_MARK, SER_PAR_SPACE
+};
+
+enum {
+    SER_FLOW_NONE, SER_FLOW_XONXOFF, SER_FLOW_RTSCTS, SER_FLOW_DSRDTR
+};
+
+/*
+ * Tables of string <-> enum value mappings used in settings.c.
+ * Defined here so that backends can export their GSS library tables
+ * to the cross-platform settings code.
+ */
+struct keyvalwhere {
+    /*
+     * Two fields which define a string and enum value to be
+     * equivalent to each other.
+     */
+    const char *s;
+    int v;
+
+    /*
+     * The next pair of fields are used by gprefs() in settings.c to
+     * arrange that when it reads a list of strings representing a
+     * preference list and translates it into the corresponding list
+     * of integers, strings not appearing in the list are entered in a
+     * configurable position rather than uniformly at the end.
+     */
+
+    /*
+     * 'vrel' indicates which other value in the list to place this
+     * element relative to. It should be a value that has occurred in
+     * a 'v' field of some other element of the array, or -1 to
+     * indicate that we simply place relative to one or other end of
+     * the list.
+     *
+     * gprefs will try to process the elements in an order which makes
+     * this field work (i.e. so that the element referenced has been
+     * added before processing this one).
+     */
+    int vrel;
+
+    /*
+     * 'where' indicates whether to place the new value before or
+     * after the one referred to by vrel. -1 means before; +1 means
+     * after.
+     *
+     * When vrel is -1, this also implicitly indicates which end of
+     * the array to use. So vrel=-1, where=-1 means to place _before_
+     * some end of the list (hence, at the last element); vrel=-1,
+     * where=+1 means to place _after_ an end (hence, at the first).
+     */
+    int where;
+};
+
+#ifndef NO_GSSAPI
+extern const int ngsslibs;
+extern const char *const gsslibnames[]; /* for displaying in configuration */
+extern const struct keyvalwhere gsslibkeywords[]; /* for settings.c */
+#endif
+
+extern const char *const ttymodes[];
+
+enum {
+    /*
+     * Network address types. Used for specifying choice of IPv4/v6
+     * in config; also used in proxy.c to indicate whether a given
+     * host name has already been resolved or will be resolved at
+     * the proxy end.
+     */
+    ADDRTYPE_UNSPEC,
+    ADDRTYPE_IPV4,
+    ADDRTYPE_IPV6,
+    ADDRTYPE_LOCAL,    /* e.g. Unix domain socket, or Windows named pipe */
+    ADDRTYPE_NAME      /* SockAddr storing an unresolved host name */
+};
+
+/* Backend flags */
+#define BACKEND_RESIZE_FORBIDDEN    0x01   /* Backend does not allow
+                                              resizing terminal */
+#define BACKEND_NEEDS_TERMINAL      0x02   /* Backend must have terminal */
+#define BACKEND_SUPPORTS_NC_HOST    0x04   /* Backend can honour
+                                              CONF_ssh_nc_host */
+#define BACKEND_NOTIFIES_SESSION_START 0x08 /* Backend will call
+                                               seat_notify_session_started */
+
+/* In (no)sshproxy.c */
+extern const bool ssh_proxy_supported;
+
+/*
+ * This structure type wraps a Seat pointer, in a way that has no
+ * purpose except to be a different type.
+ *
+ * The Seat wrapper functions that present interactive prompts all
+ * expect one of these in place of their ordinary Seat pointer. You
+ * get one by calling interactor_announce (defined below), which will
+ * print a message (if not already done) identifying the Interactor
+ * that originated the prompt.
+ *
+ * This arranges that the C type system itself will check that no call
+ * to any of those Seat methods has omitted the mandatory call to
+ * interactor_announce beforehand.
+ */
+struct InteractionReadySeat {
+    Seat *seat;
+};
+
+/*
+ * The Interactor trait is implemented by anything that is capable of
+ * presenting interactive prompts or questions to the user during
+ * network connection setup. Every Backend that ever needs to do this
+ * is an Interactor, but also, while a Backend is making its initial
+ * network connection, it may go via network proxy code which is also
+ * an Interactor and can ask questions of its own.
+ */
+struct Interactor {
+    const InteractorVtable *vt;
+
+    /* The parent Interactor that we are a proxy for, if any. */
+    Interactor *parent;
+
+    /*
+     * If we're the top-level Interactor (parent==NULL), then this
+     * field records the last Interactor that actually did anything
+     * interactive, so that we know when to announce a changeover
+     * between levels of proxying.
+     *
+     * If parent != NULL, this field is not used.
+     */
+    Interactor *last_to_talk;
+};
+
+struct InteractorVtable {
+    /*
+     * Returns a user-facing description of the nature of the network
+     * connection being made. Used in interactive proxy authentication
+     * to announce which connection attempt is now in control of the
+     * Seat.
+     *
+     * The idea is not just to be written in natural language, but to
+     * connect with the user's idea of _why_ they think some
+     * connection is being made. For example, instead of saying 'TCP
+     * connection to 123.45.67.89 port 22', you might say 'SSH
+     * connection to [logical host name for SSH host key purposes]'.
+     *
+     * The returned string must be freed by the caller.
+     */
+    char *(*description)(Interactor *itr);
+
+    /*
+     * Returns the LogPolicy associated with this Interactor. (A
+     * Backend can derive this from its logging context; a proxy
+     * Interactor inherits it from the Interactor for the parent
+     * network connection.)
+     */
+    LogPolicy *(*logpolicy)(Interactor *itr);
+
+    /*
+     * Gets and sets the Seat that this Interactor talks to. When a
+     * Seat is borrowed and replaced with a TempSeat, this will be the
+     * mechanism by which that replacement happens.
+     */
+    Seat *(*get_seat)(Interactor *itr);
+    void (*set_seat)(Interactor *itr, Seat *seat);
+};
+
+static inline char *interactor_description(Interactor *itr)
+{ return itr->vt->description(itr); }
+static inline LogPolicy *interactor_logpolicy(Interactor *itr)
+{ return itr->vt->logpolicy(itr); }
+static inline Seat *interactor_get_seat(Interactor *itr)
+{ return itr->vt->get_seat(itr); }
+static inline void interactor_set_seat(Interactor *itr, Seat *seat)
+{ itr->vt->set_seat(itr, seat); }
+
+static inline void interactor_set_child(Interactor *parent, Interactor *child)
+{ child->parent = parent; }
+Seat *interactor_borrow_seat(Interactor *itr);
+void interactor_return_seat(Interactor *itr);
+InteractionReadySeat interactor_announce(Interactor *itr);
+
+/* Interactors that are Backends will find this helper function useful
+ * in constructing their description strings */
+char *default_description(const BackendVtable *backvt,
+                          const char *host, int port);
+
+/*
+ * The Backend trait is the top-level one that governs each of the
+ * user-facing main modes that PuTTY can use to talk to some
+ * destination: SSH, Telnet, serial port, pty, etc.
+ */
+
+struct Backend {
+    const BackendVtable *vt;
+
+    /* Many Backends are also Interactors. If this one is, a pointer
+     * to its Interactor trait lives here. */
+    Interactor *interactor;
+};
+struct BackendVtable {
+    char *(*init) (const BackendVtable *vt, Seat *seat,
+                   Backend **backend_out, LogContext *logctx, Conf *conf,
+                   const char *host, int port, char **realhost,
+                   bool nodelay, bool keepalive);
+
+    void (*free) (Backend *be);
+    /* Pass in a replacement configuration. */
+    void (*reconfig) (Backend *be, Conf *conf);
+    void (*send) (Backend *be, const char *buf, size_t len);
+    /* sendbuffer() returns the current amount of buffered data */
+    size_t (*sendbuffer) (Backend *be);
+    void (*size) (Backend *be, int width, int height);
+    void (*special) (Backend *be, SessionSpecialCode code, int arg);
+    const SessionSpecial *(*get_specials) (Backend *be);
+    bool (*connected) (Backend *be);
+    int (*exitcode) (Backend *be);
+    /* If back->sendok() returns false, the backend doesn't currently
+     * want input data, so the frontend should avoid acquiring any if
+     * possible (passing back-pressure on to its sender).
+     *
+     * Policy rule: no backend shall return true from sendok() while
+     * its network connection attempt is still ongoing. This ensures
+     * that if making the network connection involves a proxy type
+     * which wants to interact with the user via the terminal, the
+     * proxy implementation and the backend itself won't fight over
+     * who gets the terminal input. */
+    bool (*sendok) (Backend *be);
+    bool (*ldisc_option_state) (Backend *be, int);
+    void (*provide_ldisc) (Backend *be, Ldisc *ldisc);
+    /* Tells the back end that the front end  buffer is clearing. */
+    void (*unthrottle) (Backend *be, size_t bufsize);
+    int (*cfg_info) (Backend *be);
+
+    /* Only implemented in the SSH protocol: check whether a
+     * connection-sharing upstream exists for a given configuration. */
+    bool (*test_for_upstream)(const char *host, int port, Conf *conf);
+    /* Special-purpose function to return additional information to put
+     * in a "are you sure you want to close this session" dialog;
+     * return NULL if no such info, otherwise caller must free.
+     * Only implemented in the SSH protocol, to warn about downstream
+     * connections that would be lost if this one were terminated. */
+    char *(*close_warn_text)(Backend *be);
+
+    /* 'id' is a machine-readable name for the backend, used in
+     * saved-session storage. 'displayname_tc' and 'displayname_lc'
+     * are human-readable names, one in title-case for config boxes,
+     * and one in lower-case for use in mid-sentence. */
+    const char *id, *displayname_tc, *displayname_lc;
+
+    int protocol;
+    int default_port;
+    unsigned flags;
+
+    /* Only relevant for the serial protocol: bit masks of which
+     * parity and flow control settings are supported. */
+    unsigned serial_parity_mask, serial_flow_mask;
+};
+
+static inline char *backend_init(
+    const BackendVtable *vt, Seat *seat, Backend **out, LogContext *logctx,
+    Conf *conf, const char *host, int port, char **rhost, bool nd, bool ka)
+{ return vt->init(vt, seat, out, logctx, conf, host, port, rhost, nd, ka); }
+static inline void backend_free(Backend *be)
+{ be->vt->free(be); }
+static inline void backend_reconfig(Backend *be, Conf *conf)
+{ be->vt->reconfig(be, conf); }
+static inline void backend_send(Backend *be, const char *buf, size_t len)
+{ be->vt->send(be, buf, len); }
+static inline size_t backend_sendbuffer(Backend *be)
+{ return be->vt->sendbuffer(be); }
+static inline void backend_size(Backend *be, int width, int height)
+{ be->vt->size(be, width, height); }
+static inline void backend_special(
+    Backend *be, SessionSpecialCode code, int arg)
+{ be->vt->special(be, code, arg); }
+static inline const SessionSpecial *backend_get_specials(Backend *be)
+{ return be->vt->get_specials(be); }
+static inline bool backend_connected(Backend *be)
+{ return be->vt->connected(be); }
+static inline int backend_exitcode(Backend *be)
+{ return be->vt->exitcode(be); }
+static inline bool backend_sendok(Backend *be)
+{ return be->vt->sendok(be); }
+static inline bool backend_ldisc_option_state(Backend *be, int state)
+{ return be->vt->ldisc_option_state(be, state); }
+static inline void backend_provide_ldisc(Backend *be, Ldisc *ldisc)
+{ be->vt->provide_ldisc(be, ldisc); }
+static inline void backend_unthrottle(Backend *be, size_t bufsize)
+{ be->vt->unthrottle(be, bufsize); }
+static inline int backend_cfg_info(Backend *be)
+{ return be->vt->cfg_info(be); }
+
+extern const struct BackendVtable *const backends[];
+/*
+ * In programs with a config UI, only the first few members of
+ * backends[] will be displayed at the top-level; the others will be
+ * relegated to a drop-down.
+ */
+extern const size_t n_ui_backends;
+
+/*
+ * Suggested default protocol provided by the backend link module.
+ * The application is free to ignore this.
+ */
+extern const int be_default_protocol;
+
+/*
+ * Name of this particular application, for use in the config box
+ * and other pieces of text.
+ */
+extern const char *const appname;
+
+/*
+ * Used by callback.c; declared up here so that prompts_t can use it
+ */
+typedef void (*toplevel_callback_fn_t)(void *ctx);
+
+/* Enum of result types in SeatPromptResult below */
+typedef enum SeatPromptResultKind {
+    /* Answer not yet available at all; either try again later or wait
+     * for a callback (depending on the request's API) */
+    SPRK_INCOMPLETE,
+
+    /* We're abandoning the connection because the user interactively
+     * told us to. (Hence, no need to present an error message
+     * telling the user we're doing that: they already know.) */
+    SPRK_USER_ABORT,
+
+    /* We're abandoning the connection for some other reason (e.g. we
+     * were unable to present the prompt at all, or a batch-mode
+     * configuration told us to give the answer no). This may
+     * ultimately have stemmed from some user configuration, but they
+     * didn't _tell us right now_ to abandon this connection, so we
+     * still need to inform them that we've done so. */
+    SPRK_SW_ABORT,
+
+    /* We're proceeding with the connection and have all requested
+     * information (if any) */
+    SPRK_OK
+} SeatPromptResultKind;
+
+/* Small struct to present the results of interactive requests from
+ * backend to Seat (see below) */
+struct SeatPromptResult {
+    SeatPromptResultKind kind;
+
+    /*
+     * In the case of SPRK_SW_ABORT, the frontend provides an error
+     * message to present to the user. But dynamically allocating it
+     * up front would mean having to make sure it got freed at any
+     * call site where one of these structs is received (and freed
+     * _once_ no matter how many times the struct is copied). So
+     * instead we provide a function that will generate the error
+     * message into a BinarySink.
+     */
+    void (*errfn)(SeatPromptResult, BinarySink *);
+
+    /*
+     * And some fields the error function can use to construct the
+     * message (holding, e.g. an OS error code).
+     */
+    const char *errdata_lit; /* statically allocated, e.g. a string literal */
+    unsigned errdata_u;
+};
+
+/* Helper function to construct the simple versions of these
+ * structures inline */
+static inline SeatPromptResult make_spr_simple(SeatPromptResultKind kind)
+{
+    SeatPromptResult spr;
+    spr.kind = kind;
+    spr.errdata_lit = NULL;
+    return spr;
+}
+
+/* Most common constructor function for SPRK_SW_ABORT errors */
+SeatPromptResult make_spr_sw_abort_static(const char *);
+
+/* Convenience macros wrapping those constructors in turn */
+#define SPR_INCOMPLETE make_spr_simple(SPRK_INCOMPLETE)
+#define SPR_USER_ABORT make_spr_simple(SPRK_USER_ABORT)
+#define SPR_SW_ABORT(lit) make_spr_sw_abort_static(lit)
+#define SPR_OK make_spr_simple(SPRK_OK)
+
+/* Query function that folds both kinds of abort together */
+static inline bool spr_is_abort(SeatPromptResult spr)
+{
+    return spr.kind == SPRK_USER_ABORT || spr.kind == SPRK_SW_ABORT;
+}
+
+/* Function to return a dynamically allocated copy of the error message */
+char *spr_get_error_message(SeatPromptResult spr);
+
+/*
+ * Mechanism for getting text strings such as usernames and passwords
+ * from the front-end.
+ * The fields are mostly modelled after SSH's keyboard-interactive auth.
+ * FIXME We should probably mandate a character set/encoding (probably UTF-8).
+ *
+ * Since many of the pieces of text involved may be chosen by the server,
+ * the caller must take care to ensure that the server can't spoof locally-
+ * generated prompts such as key passphrase prompts. Some ground rules:
+ *  - If the front-end needs to truncate a string, it should lop off the
+ *    end.
+ *  - The front-end should filter out any dangerous characters and
+ *    generally not trust the strings. (But \n is required to behave
+ *    vaguely sensibly, at least in `instruction', and ideally in
+ *    `prompt[]' too.)
+ */
+typedef struct {
+    char *prompt;
+    bool echo;
+    strbuf *result;
+} prompt_t;
+typedef struct prompts_t prompts_t;
+struct prompts_t {
+    /*
+     * Indicates whether the information entered is to be used locally
+     * (for instance a key passphrase prompt), or is destined for the wire.
+     * This is a hint only; the front-end is at liberty not to use this
+     * information (so the caller should ensure that the supplied text is
+     * sufficient).
+     */
+    bool to_server;
+
+    /*
+     * Indicates whether the prompts originated _at_ the server, so
+     * that the front end can display some kind of trust sigil that
+     * distinguishes (say) a legit private-key passphrase prompt from
+     * a fake one sent by a malicious server.
+     */
+    bool from_server;
+
+    char *name;         /* Short description, perhaps for dialog box title */
+    bool name_reqd;     /* Display of `name' required or optional? */
+    char *instruction;  /* Long description, maybe with embedded newlines */
+    bool instr_reqd;    /* Display of `instruction' required or optional? */
+    size_t n_prompts;   /* May be zero (in which case display the foregoing,
+                         * if any, and return success) */
+    size_t prompts_size; /* allocated storage capacity for prompts[] */
+    prompt_t **prompts;
+    void *data;         /* slot for housekeeping data, managed by
+                         * seat_get_userpass_input(); initially NULL */
+    SeatPromptResult spr; /* some implementations need to cache one of these */
+
+    /*
+     * Callback you can fill in to be notified when all the prompts'
+     * responses are available. After you receive this notification, a
+     * further call to the get_userpass_input function will return the
+     * final state of the prompts system, which is guaranteed not to
+     * be negative for 'still ongoing'.
+     */
+    toplevel_callback_fn_t callback;
+    void *callback_ctx;
+
+    /*
+     * When this prompts_t is known to an Ldisc, we might need to
+     * break the connection if things get freed in an emergency. So
+     * this is a pointer to the Ldisc's pointer to us.
+     */
+    prompts_t **ldisc_ptr_to_us;
+};
+prompts_t *new_prompts(void);
+void add_prompt(prompts_t *p, char *promptstr, bool echo);
+void prompt_set_result(prompt_t *pr, const char *newstr);
+char *prompt_get_result(prompt_t *pr);
+const char *prompt_get_result_ref(prompt_t *pr);
+void free_prompts(prompts_t *p);
+
+/*
+ * Data type definitions for true-colour terminal display.
+ * 'optionalrgb' describes a single RGB colour, which overrides the
+ * other colour settings if 'enabled' is nonzero, and is ignored
+ * otherwise. 'truecolour' contains a pair of those for foreground and
+ * background.
+ */
+typedef struct optionalrgb {
+    bool enabled;
+    unsigned char r, g, b;
+} optionalrgb;
+extern const optionalrgb optionalrgb_none;
+typedef struct truecolour {
+    optionalrgb fg, bg;
+} truecolour;
+#define optionalrgb_equal(r1,r2) (                              \
+        (r1).enabled==(r2).enabled &&                           \
+        (r1).r==(r2).r && (r1).g==(r2).g && (r1).b==(r2).b)
+#define truecolour_equal(c1,c2) (               \
+        optionalrgb_equal((c1).fg, (c2).fg) &&  \
+        optionalrgb_equal((c1).bg, (c2).bg))
+
+/*
+ * Enumeration of clipboards. We provide some standard ones cross-
+ * platform, and then permit each platform to extend this enumeration
+ * further by defining PLATFORM_CLIPBOARDS in its own header file.
+ *
+ * CLIP_NULL is a non-clipboard, writes to which are ignored and reads
+ * from which return no data.
+ *
+ * CLIP_LOCAL refers to a buffer within terminal.c, which
+ * unconditionally saves the last data selected in the terminal. In
+ * configurations where a system clipboard is not written
+ * automatically on selection but instead by an explicit UI action,
+ * this is where the code responding to that action can find the data
+ * to write to the clipboard in question.
+ */
+#define CROSS_PLATFORM_CLIPBOARDS(X)                    \
+    X(CLIP_NULL, "null clipboard")                      \
+    X(CLIP_LOCAL, "last text selected in terminal")     \
+    /* end of list */
+
+#define ALL_CLIPBOARDS(X)                       \
+    CROSS_PLATFORM_CLIPBOARDS(X)                \
+    PLATFORM_CLIPBOARDS(X)                      \
+    /* end of list */
+
+#define CLIP_ID(id,name) id,
+enum { ALL_CLIPBOARDS(CLIP_ID) N_CLIPBOARDS };
+#undef CLIP_ID
+
+/* Hint from backend to frontend about time-consuming operations, used
+ * by seat_set_busy_status. Initial state is assumed to be
+ * BUSY_NOT. */
+typedef enum BusyStatus {
+    BUSY_NOT,       /* Not busy, all user interaction OK */
+    BUSY_WAITING,   /* Waiting for something; local event loops still
+                       running so some local interaction (e.g. menus)
+                       OK, but network stuff is suspended */
+    BUSY_CPU        /* Locally busy (e.g. crypto); user interaction
+                     * suspended */
+} BusyStatus;
+
+typedef enum SeatInteractionContext {
+    SIC_BANNER, SIC_KI_PROMPTS
+} SeatInteractionContext;
+
+typedef enum SeatOutputType {
+    SEAT_OUTPUT_STDOUT, SEAT_OUTPUT_STDERR
+} SeatOutputType;
+
+typedef enum SeatDialogTextType {
+    SDT_PARA, SDT_DISPLAY, SDT_SCARY_HEADING,
+    SDT_TITLE, SDT_PROMPT, SDT_BATCH_ABORT,
+    SDT_MORE_INFO_KEY, SDT_MORE_INFO_VALUE_SHORT, SDT_MORE_INFO_VALUE_BLOB
+} SeatDialogTextType;
+struct SeatDialogTextItem {
+    SeatDialogTextType type;
+    char *text;
+};
+struct SeatDialogText {
+    size_t nitems, itemsize;
+    SeatDialogTextItem *items;
+};
+SeatDialogText *seat_dialog_text_new(void);
+void seat_dialog_text_free(SeatDialogText *sdt);
+PRINTF_LIKE(3, 4) void seat_dialog_text_append(
+    SeatDialogText *sdt, SeatDialogTextType type, const char *fmt, ...);
+
+/*
+ * Data type 'Seat', which is an API intended to contain essentially
+ * everything that a back end might need to talk to its client for:
+ * session output, password prompts, SSH warnings about host keys and
+ * weak cryptography, notifications of events like the remote process
+ * exiting or the GUI specials menu needing an update.
+ */
+struct Seat {
+    const struct SeatVtable *vt;
+};
+struct SeatVtable {
+    /*
+     * Provide output from the remote session. 'type' indicates the
+     * type of the output (stdout or stderr), which can be used to
+     * split the output into separate message channels, if the seat
+     * wants to handle them differently. But combining the channels
+     * into one is OK too; that's what terminal-window based seats do.
+     *
+     * The return value is the current size of the output backlog.
+     */
+    size_t (*output)(Seat *seat, SeatOutputType type,
+                     const void *data, size_t len);
+
+    /*
+     * Called when the back end wants to indicate that EOF has arrived
+     * on the server-to-client stream. Returns false to indicate that
+     * we intend to keep the session open in the other direction, or
+     * true to indicate that if they're closing so are we.
+     */
+    bool (*eof)(Seat *seat);
+
+    /*
+     * Called by the back end to notify that the output backlog has
+     * changed size. A front end in control of the event loop won't
+     * necessarily need this (they can just keep checking it via
+     * backend_sendbuffer at every opportunity), but one buried in the
+     * depths of something else (like an SSH proxy) will need to be
+     * proactively notified that the amount of buffered data has
+     * become smaller.
+     */
+    void (*sent)(Seat *seat, size_t new_sendbuffer);
+
+    /*
+     * Provide authentication-banner output from the session setup.
+     * End-user Seats can treat this as very similar to 'output', but
+     * intermediate Seats in complex proxying situations will want to
+     * implement this and 'output' differently.
+     */
+    size_t (*banner)(Seat *seat, const void *data, size_t len);
+
+    /*
+     * Try to get answers from a set of interactive login prompts. The
+     * prompts are provided in 'p'.
+     *
+     * (FIXME: it would be nice to distinguish two classes of user-
+     * abort action, so the user could specify 'I want to abandon this
+     * entire attempt to start a session' or the milder 'I want to
+     * abandon this particular form of authentication and fall back to
+     * a different one' - e.g. if you turn out not to be able to
+     * remember your private key passphrase then perhaps you'd rather
+     * fall back to password auth rather than aborting the whole
+     * session.)
+     */
+    SeatPromptResult (*get_userpass_input)(Seat *seat, prompts_t *p);
+
+    /*
+     * Notify the seat that the main session channel has been
+     * successfully set up.
+     *
+     * This is only used as part of the SSH proxying system, so it's
+     * not necessary to implement it in all backends. A backend must
+     * call this if it advertises the BACKEND_NOTIFIES_SESSION_START
+     * flag, and otherwise, doesn't have to.
+     */
+    void (*notify_session_started)(Seat *seat);
+
+    /*
+     * Notify the seat that the process running at the other end of
+     * the connection has finished.
+     */
+    void (*notify_remote_exit)(Seat *seat);
+
+    /*
+     * Notify the seat that the whole connection has finished.
+     * (Distinct from notify_remote_exit, e.g. in the case where you
+     * have port forwardings still active when the main foreground
+     * session goes away: then you'd get notify_remote_exit when the
+     * foreground session dies, but notify_remote_disconnect when the
+     * last forwarding vanishes and the network connection actually
+     * closes.)
+     *
+     * This function might be called multiple times by accident; seats
+     * should be prepared to cope.
+     *
+     * More precisely: this function notifies the seat that
+     * backend_connected() might now return false where previously it
+     * returned true. (Note the 'might': an accidental duplicate call
+     * might happen when backend_connected() was already returning
+     * false. Or even, in weird situations, when it hadn't stopped
+     * returning true yet. The point is, when you get this
+     * notification, all it's really telling you is that it's worth
+     * _checking_ backend_connected, if you weren't already.)
+     */
+    void (*notify_remote_disconnect)(Seat *seat);
+
+    /*
+     * Notify the seat that the connection has suffered a fatal error.
+     */
+    void (*connection_fatal)(Seat *seat, const char *message);
+
+    /*
+     * Notify the seat that the list of special commands available
+     * from backend_get_specials() has changed, so that it might want
+     * to call that function to repopulate its menu.
+     *
+     * Seats are not expected to call backend_get_specials()
+     * proactively; they may start by assuming that the backend
+     * provides no special commands at all, so if the backend does
+     * provide any, then it should use this notification at startup
+     * time. Of course it can also invoke it later if the set of
+     * special commands changes.
+     *
+     * It does not need to invoke it at session shutdown.
+     */
+    void (*update_specials_menu)(Seat *seat);
+
+    /*
+     * Get the seat's preferred value for an SSH terminal mode
+     * setting. Returning NULL indicates no preference (i.e. the SSH
+     * connection will not attempt to set the mode at all).
+     *
+     * The returned value is dynamically allocated, and the caller
+     * should free it.
+     */
+    char *(*get_ttymode)(Seat *seat, const char *mode);
+
+    /*
+     * Tell the seat whether the backend is currently doing anything
+     * CPU-intensive (typically a cryptographic key exchange). See
+     * BusyStatus enumeration above.
+     */
+    void (*set_busy_status)(Seat *seat, BusyStatus status);
+
+    /*
+     * Ask the seat whether a given SSH host key should be accepted.
+     * This is called after we've already checked it by any means we
+     * can do ourselves, such as checking against host key
+     * fingerprints in the Conf or the host key cache on disk: once we
+     * call this function, we've already decided there's nothing for
+     * it but to prompt the user.
+     *
+     * 'mismatch' reports the result of checking the host key cache:
+     * it is true if the server has presented a host key different
+     * from the one we expected, and false if we had no expectation in
+     * the first place.
+     *
+     * This call may prompt the user synchronously and not return
+     * until the answer is available, or it may present the prompt and
+     * return immediately, giving the answer later via the provided
+     * callback.
+     *
+     * Return values:
+     *
+     *  - +1 means `user approved the key, so continue with the
+     *    connection'
+     *
+     *  - 0 means `user rejected the key, abandon the connection'
+     *
+     *  - -1 means `I've initiated enquiries, please wait to be called
+     *    back via the provided function with a result that's either 0
+     *    or +1'.
+     */
+    SeatPromptResult (*confirm_ssh_host_key)(
+        Seat *seat, const char *host, int port, const char *keytype,
+        char *keystr, SeatDialogText *text, HelpCtx helpctx,
+        void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+
+    /*
+     * Check with the seat whether it's OK to use a cryptographic
+     * primitive from below the 'warn below this line' threshold in
+     * the input Conf. Return values are the same as
+     * confirm_ssh_host_key above.
+     */
+    SeatPromptResult (*confirm_weak_crypto_primitive)(
+        Seat *seat, const char *algtype, const char *algname,
+        void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+
+    /*
+     * Variant form of confirm_weak_crypto_primitive, which prints a
+     * slightly different message but otherwise has the same
+     * semantics.
+     *
+     * This form is used in the case where we're using a host key
+     * below the warning threshold because that's the best one we have
+     * cached, but at least one host key algorithm *above* the
+     * threshold is available that we don't have cached. 'betteralgs'
+     * lists the better algorithm(s).
+     */
+    SeatPromptResult (*confirm_weak_cached_hostkey)(
+        Seat *seat, const char *algname, const char *betteralgs,
+        void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+
+    /*
+     * Some snippets of text describing the UI actions in host key
+     * prompts / dialog boxes, to be used in ssh/common.c when it
+     * assembles the full text of those prompts.
+     */
+    const SeatDialogPromptDescriptions *(*prompt_descriptions)(Seat *seat);
+
+    /*
+     * Indicates whether the seat is expecting to interact with the
+     * user in the UTF-8 character set. (Affects e.g. visual erase
+     * handling in local line editing.)
+     */
+    bool (*is_utf8)(Seat *seat);
+
+    /*
+     * Notify the seat that the back end, and/or the ldisc between
+     * them, have changed their idea of whether they currently want
+     * local echo and/or local line editing enabled.
+     */
+    void (*echoedit_update)(Seat *seat, bool echoing, bool editing);
+
+    /*
+     * Return the local X display string relevant to a seat, or NULL
+     * if there isn't one or if the concept is meaningless.
+     */
+    const char *(*get_x_display)(Seat *seat);
+
+    /*
+     * Return the X11 id of the X terminal window relevant to a seat,
+     * by returning true and filling in the output pointer. Return
+     * false if there isn't one or if the concept is meaningless.
+     */
+    bool (*get_windowid)(Seat *seat, long *id_out);
+
+    /*
+     * Return the size of the terminal window in pixels. If the
+     * concept is meaningless or the information is unavailable,
+     * return false; otherwise fill in the output pointers and return
+     * true.
+     */
+    bool (*get_window_pixel_size)(Seat *seat, int *width, int *height);
+
+    /*
+     * Return a StripCtrlChars appropriate for sanitising untrusted
+     * terminal data (e.g. SSH banners, prompts) being sent to the
+     * user of this seat. May return NULL if no sanitisation is
+     * needed.
+     */
+    StripCtrlChars *(*stripctrl_new)(
+        Seat *seat, BinarySink *bs_out, SeatInteractionContext sic);
+
+    /*
+     * Set the seat's current idea of where output is coming from.
+     * True means that output is being generated by our own code base
+     * (and hence, can be trusted if it's asking you for secrets such
+     * as your passphrase); false means output is coming from the
+     * server.
+     */
+    void (*set_trust_status)(Seat *seat, bool trusted);
+
+    /*
+     * Query whether this Seat can do anything user-visible in
+     * response to set_trust_status.
+     *
+     * Returns true if the seat has a way to indicate this
+     * distinction. Returns false if not, in which case the backend
+     * should use a fallback defence against spoofing of PuTTY's local
+     * prompts by malicious servers.
+     */
+    bool (*can_set_trust_status)(Seat *seat);
+
+    /*
+     * Query whether this Seat's interactive prompt responses and its
+     * session input come from the same place.
+     *
+     * If false, this is used to suppress the final 'Press Return to
+     * begin session' anti-spoofing prompt in Plink. For example,
+     * Plink itself sets this flag if its standard input is redirected
+     * (and therefore not coming from the same place as the console
+     * it's sending its prompts to).
+     */
+    bool (*has_mixed_input_stream)(Seat *seat);
+
+    /*
+     * Ask the seat whether it would like verbose messages.
+     */
+    bool (*verbose)(Seat *seat);
+
+    /*
+     * Ask the seat whether it's an interactive program.
+     */
+    bool (*interactive)(Seat *seat);
+
+    /*
+     * Return the seat's current idea of where the output cursor is.
+     *
+     * Returns true if the seat has a cursor. Returns false if not.
+     */
+    bool (*get_cursor_position)(Seat *seat, int *x, int *y);
+};
+
+static inline size_t seat_output(
+    Seat *seat, SeatOutputType type, const void *data, size_t len)
+{ return seat->vt->output(seat, type, data, len); }
+static inline bool seat_eof(Seat *seat)
+{ return seat->vt->eof(seat); }
+static inline void seat_sent(Seat *seat, size_t bufsize)
+{ seat->vt->sent(seat, bufsize); }
+static inline size_t seat_banner(
+    InteractionReadySeat iseat, const void *data, size_t len)
+{ return iseat.seat->vt->banner(iseat.seat, data, len); }
+static inline SeatPromptResult seat_get_userpass_input(
+    InteractionReadySeat iseat, prompts_t *p)
+{ return iseat.seat->vt->get_userpass_input(iseat.seat, p); }
+static inline void seat_notify_session_started(Seat *seat)
+{ seat->vt->notify_session_started(seat); }
+static inline void seat_notify_remote_exit(Seat *seat)
+{ seat->vt->notify_remote_exit(seat); }
+static inline void seat_notify_remote_disconnect(Seat *seat)
+{ seat->vt->notify_remote_disconnect(seat); }
+static inline void seat_update_specials_menu(Seat *seat)
+{ seat->vt->update_specials_menu(seat); }
+static inline char *seat_get_ttymode(Seat *seat, const char *mode)
+{ return seat->vt->get_ttymode(seat, mode); }
+static inline void seat_set_busy_status(Seat *seat, BusyStatus status)
+{ seat->vt->set_busy_status(seat, status); }
+static inline SeatPromptResult seat_confirm_ssh_host_key(
+    InteractionReadySeat iseat, const char *h, int p, const char *ktyp,
+    char *kstr, SeatDialogText *text, HelpCtx helpctx,
+    void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
+{ return iseat.seat->vt->confirm_ssh_host_key(
+        iseat.seat, h, p, ktyp, kstr, text, helpctx, cb, ctx); }
+static inline SeatPromptResult seat_confirm_weak_crypto_primitive(
+    InteractionReadySeat iseat, const char *atyp, const char *aname,
+    void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
+{ return iseat.seat->vt->confirm_weak_crypto_primitive(
+        iseat.seat, atyp, aname, cb, ctx); }
+static inline SeatPromptResult seat_confirm_weak_cached_hostkey(
+    InteractionReadySeat iseat, const char *aname, const char *better,
+    void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
+{ return iseat.seat->vt->confirm_weak_cached_hostkey(
+        iseat.seat, aname, better, cb, ctx); }
+static inline const SeatDialogPromptDescriptions *seat_prompt_descriptions(
+    Seat *seat)
+{ return seat->vt->prompt_descriptions(seat); }
+static inline bool seat_is_utf8(Seat *seat)
+{ return seat->vt->is_utf8(seat); }
+static inline void seat_echoedit_update(Seat *seat, bool ec, bool ed)
+{ seat->vt->echoedit_update(seat, ec, ed); }
+static inline const char *seat_get_x_display(Seat *seat)
+{ return seat->vt->get_x_display(seat); }
+static inline bool seat_get_windowid(Seat *seat, long *id_out)
+{ return seat->vt->get_windowid(seat, id_out); }
+static inline bool seat_get_window_pixel_size(Seat *seat, int *w, int *h)
+{ return seat->vt->get_window_pixel_size(seat, w, h); }
+static inline StripCtrlChars *seat_stripctrl_new(
+    Seat *seat, BinarySink *bs, SeatInteractionContext sic)
+{ return seat->vt->stripctrl_new(seat, bs, sic); }
+static inline void seat_set_trust_status(Seat *seat, bool trusted)
+{ seat->vt->set_trust_status(seat, trusted); }
+static inline bool seat_can_set_trust_status(Seat *seat)
+{ return seat->vt->can_set_trust_status(seat); }
+static inline bool seat_has_mixed_input_stream(Seat *seat)
+{ return seat->vt->has_mixed_input_stream(seat); }
+static inline bool seat_verbose(Seat *seat)
+{ return seat->vt->verbose(seat); }
+static inline bool seat_interactive(Seat *seat)
+{ return seat->vt->interactive(seat); }
+static inline bool seat_get_cursor_position(Seat *seat, int *x, int *y)
+{ return  seat->vt->get_cursor_position(seat, x, y); }
+
+/* Unlike the seat's actual method, the public entry point
+ * seat_connection_fatal is a wrapper function with a printf-like API,
+ * defined in utils. */
+void seat_connection_fatal(Seat *seat, const char *fmt, ...) PRINTF_LIKE(2, 3);
+
+/* Handy aliases for seat_output which set is_stderr to a fixed value. */
+static inline size_t seat_stdout(Seat *seat, const void *data, size_t len)
+{ return seat_output(seat, SEAT_OUTPUT_STDOUT, data, len); }
+static inline size_t seat_stdout_pl(Seat *seat, ptrlen data)
+{ return seat_output(seat, SEAT_OUTPUT_STDOUT, data.ptr, data.len); }
+static inline size_t seat_stderr(Seat *seat, const void *data, size_t len)
+{ return seat_output(seat, SEAT_OUTPUT_STDERR, data, len); }
+static inline size_t seat_stderr_pl(Seat *seat, ptrlen data)
+{ return seat_output(seat, SEAT_OUTPUT_STDERR, data.ptr, data.len); }
+
+/* Alternative API for seat_banner taking a ptrlen */
+static inline size_t seat_banner_pl(InteractionReadySeat iseat, ptrlen data)
+{ return iseat.seat->vt->banner(iseat.seat, data.ptr, data.len); }
+
+struct SeatDialogPromptDescriptions {
+    const char *hk_accept_action;
+    const char *hk_connect_once_action;
+    const char *hk_cancel_action, *hk_cancel_action_Participle;
+};
+
+/* In the utils subdir: print a message to the Seat which can't be
+ * spoofed by server-supplied auth-time output such as SSH banners */
+void seat_antispoof_msg(InteractionReadySeat iseat, const char *msg);
+
+/*
+ * Stub methods for seat implementations that want to use the obvious
+ * null handling for a given method.
+ *
+ * These are generally obvious, except for is_utf8, where you might
+ * plausibly want to return either fixed answer 'no' or 'yes'.
+ */
+size_t nullseat_output(
+    Seat *seat, SeatOutputType type, const void *data, size_t len);
+bool nullseat_eof(Seat *seat);
+void nullseat_sent(Seat *seat, size_t bufsize);
+size_t nullseat_banner(Seat *seat, const void *data, size_t len);
+size_t nullseat_banner_to_stderr(Seat *seat, const void *data, size_t len);
+SeatPromptResult nullseat_get_userpass_input(Seat *seat, prompts_t *p);
+void nullseat_notify_session_started(Seat *seat);
+void nullseat_notify_remote_exit(Seat *seat);
+void nullseat_notify_remote_disconnect(Seat *seat);
+void nullseat_connection_fatal(Seat *seat, const char *message);
+void nullseat_update_specials_menu(Seat *seat);
+char *nullseat_get_ttymode(Seat *seat, const char *mode);
+void nullseat_set_busy_status(Seat *seat, BusyStatus status);
+SeatPromptResult nullseat_confirm_ssh_host_key(
+    Seat *seat, const char *host, int port, const char *keytype,
+    char *keystr, SeatDialogText *text, HelpCtx helpctx,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+SeatPromptResult nullseat_confirm_weak_crypto_primitive(
+    Seat *seat, const char *algtype, const char *algname,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+SeatPromptResult nullseat_confirm_weak_cached_hostkey(
+    Seat *seat, const char *algname, const char *betteralgs,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+const SeatDialogPromptDescriptions *nullseat_prompt_descriptions(Seat *seat);
+bool nullseat_is_never_utf8(Seat *seat);
+bool nullseat_is_always_utf8(Seat *seat);
+void nullseat_echoedit_update(Seat *seat, bool echoing, bool editing);
+const char *nullseat_get_x_display(Seat *seat);
+bool nullseat_get_windowid(Seat *seat, long *id_out);
+bool nullseat_get_window_pixel_size(Seat *seat, int *width, int *height);
+StripCtrlChars *nullseat_stripctrl_new(
+    Seat *seat, BinarySink *bs_out, SeatInteractionContext sic);
+void nullseat_set_trust_status(Seat *seat, bool trusted);
+bool nullseat_can_set_trust_status_yes(Seat *seat);
+bool nullseat_can_set_trust_status_no(Seat *seat);
+bool nullseat_has_mixed_input_stream_yes(Seat *seat);
+bool nullseat_has_mixed_input_stream_no(Seat *seat);
+bool nullseat_verbose_no(Seat *seat);
+bool nullseat_verbose_yes(Seat *seat);
+bool nullseat_interactive_no(Seat *seat);
+bool nullseat_interactive_yes(Seat *seat);
+bool nullseat_get_cursor_position(Seat *seat, int *x, int *y);
+
+/*
+ * Seat functions provided by the platform's console-application
+ * support module (console.c in each platform subdirectory).
+ */
+
+void console_connection_fatal(Seat *seat, const char *message);
+SeatPromptResult console_confirm_ssh_host_key(
+    Seat *seat, const char *host, int port, const char *keytype,
+    char *keystr, SeatDialogText *text, HelpCtx helpctx,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+SeatPromptResult console_confirm_weak_crypto_primitive(
+    Seat *seat, const char *algtype, const char *algname,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+SeatPromptResult console_confirm_weak_cached_hostkey(
+    Seat *seat, const char *algname, const char *betteralgs,
+    void (*callback)(void *ctx, SeatPromptResult result), void *ctx);
+StripCtrlChars *console_stripctrl_new(
+    Seat *seat, BinarySink *bs_out, SeatInteractionContext sic);
+void console_set_trust_status(Seat *seat, bool trusted);
+bool console_can_set_trust_status(Seat *seat);
+bool console_has_mixed_input_stream(Seat *seat);
+const SeatDialogPromptDescriptions *console_prompt_descriptions(Seat *seat);
+
+/*
+ * Other centralised seat functions.
+ */
+SeatPromptResult filexfer_get_userpass_input(Seat *seat, prompts_t *p);
+bool cmdline_seat_verbose(Seat *seat);
+
+/*
+ * TempSeat: a seat implementation that can be given to a backend
+ * temporarily while network proxy setup is using the real seat.
+ * Buffers output and trust-status changes until the real seat is
+ * available again.
+ */
+
+/* Called by the proxy code to make a TempSeat. */
+Seat *tempseat_new(Seat *real);
+
+/* Query functions to tell if a Seat _is_ temporary, and if so, to
+ * return the underlying real Seat. */
+bool is_tempseat(Seat *seat);
+Seat *tempseat_get_real(Seat *seat);
+
+/* Called by interactor_return_seat once the proxy connection has
+ * finished setting up (or failed), to pass on any buffered stuff to
+ * the real seat. */
+void tempseat_flush(Seat *ts);
+
+/* Frees a TempSeat, without flushing anything it has buffered. (Call
+ * this after tempseat_flush, or alternatively, when you were going to
+ * abandon the whole connection anyway.) */
+void tempseat_free(Seat *ts);
+
+typedef struct rgb {
+    uint8_t r, g, b;
+} rgb;
+
+/*
+ * Data type 'TermWin', which is a vtable encapsulating all the
+ * functionality that Terminal expects from its containing terminal
+ * window.
+ */
+struct TermWin {
+    const struct TermWinVtable *vt;
+};
+struct TermWinVtable {
+    /*
+     * All functions listed here between setup_draw_ctx and
+     * free_draw_ctx expect to be _called_ between them too, so that
+     * the TermWin has a drawing context currently available.
+     *
+     * (Yes, even char_width, because e.g. the Windows implementation
+     * of TermWin handles it by loading the currently configured font
+     * into the HDC and doing a GDI query.)
+     */
+    bool (*setup_draw_ctx)(TermWin *);
+    /* Draw text in the window, during a painting operation */
+    void (*draw_text)(TermWin *, int x, int y, wchar_t *text, int len,
+                      unsigned long attrs, int line_attrs, truecolour tc);
+    /* Draw the visible cursor. Expects you to have called do_text
+     * first (because it might just draw an underline over a character
+     * presumed to exist already), but also expects you to pass in all
+     * the details of the character under the cursor (because it might
+     * redraw it in different colours). */
+    void (*draw_cursor)(TermWin *, int x, int y, wchar_t *text, int len,
+                        unsigned long attrs, int line_attrs, truecolour tc);
+    /* Draw the sigil indicating that a line of text has come from
+     * PuTTY itself rather than the far end (defence against end-of-
+     * authentication spoofing) */
+    void (*draw_trust_sigil)(TermWin *, int x, int y);
+    int (*char_width)(TermWin *, int uc);
+    void (*free_draw_ctx)(TermWin *);
+
+    void (*set_cursor_pos)(TermWin *, int x, int y);
+
+    /* set_raw_mouse_mode instructs the front end to start sending mouse events
+     * in raw mode suitable for translating into mouse-tracking terminal data
+     * (e.g. include scroll-wheel events and don't bother to identify double-
+     * and triple-clicks). set_raw_mouse_mode_pointer instructs the front end
+     * to change the mouse pointer shape to *indicate* raw mouse mode. */
+    void (*set_raw_mouse_mode)(TermWin *, bool enable);
+    void (*set_raw_mouse_mode_pointer)(TermWin *, bool enable);
+
+    void (*set_scrollbar)(TermWin *, int total, int start, int page);
+
+    void (*bell)(TermWin *, int mode);
+
+    void (*clip_write)(TermWin *, int clipboard, wchar_t *text, int *attrs,
+                       truecolour *colours, int len, bool must_deselect);
+    void (*clip_request_paste)(TermWin *, int clipboard);
+
+    void (*refresh)(TermWin *);
+
+    /* request_resize asks the front end if the terminal can please be
+     * resized to (w,h) in characters. The front end MAY call
+     * term_size() in response to tell the terminal its new size
+     * (which MAY be the requested size, or some other size if the
+     * requested one can't be achieved). The front end MAY also not
+     * call term_size() at all. But the front end MUST reply to this
+     * request by calling term_resize_request_completed(), after the
+     * responding resize event has taken place (if any).
+     *
+     * The calls to term_size and term_resize_request_completed may be
+     * synchronous callbacks from within the call to request_resize(). */
+    void (*request_resize)(TermWin *, int w, int h);
+
+    void (*set_title)(TermWin *, const char *title, int codepage);
+    void (*set_icon_title)(TermWin *, const char *icontitle, int codepage);
+
+    /* set_minimised and set_maximised are assumed to set two
+     * independent settings, rather than a single three-way
+     * {min,normal,max} switch. The idea is that when you un-minimise
+     * the window it remembers whether to go back to normal or
+     * maximised. */
+    void (*set_minimised)(TermWin *, bool minimised);
+    void (*set_maximised)(TermWin *, bool maximised);
+    void (*move)(TermWin *, int x, int y);
+    void (*set_zorder)(TermWin *, bool top);
+
+    /* Set the colour palette that the TermWin will use to display
+     * text. One call to this function sets 'ncolours' consecutive
+     * colours in the OSC 4 sequence, starting at 'start'. */
+    void (*palette_set)(TermWin *, unsigned start, unsigned ncolours,
+                        const rgb *colours);
+
+    /* Query the front end for any OS-local overrides to the default
+     * colours stored in Conf. The front end should set any it cares
+     * about by calling term_palette_override.
+     *
+     * The Terminal object is passed in as a parameter, because this
+     * can be called as a callback from term_init(). So the TermWin
+     * itself won't yet have been told where to find its Terminal
+     * object, because that doesn't happen until term_init
+     * returns. */
+    void (*palette_get_overrides)(TermWin *, Terminal *);
+
+    /* Notify the front end that the terminal's buffer of unprocessed
+     * output has reduced. (Front ends will likely pass this straight
+     * on to backend_unthrottle.) */
+    void (*unthrottle)(TermWin *, size_t bufsize);
+};
+
+static inline bool win_setup_draw_ctx(TermWin *win)
+{ return win->vt->setup_draw_ctx(win); }
+static inline void win_draw_text(
+    TermWin *win, int x, int y, wchar_t *text, int len,
+    unsigned long attrs, int line_attrs, truecolour tc)
+{ win->vt->draw_text(win, x, y, text, len, attrs, line_attrs, tc); }
+static inline void win_draw_cursor(
+    TermWin *win, int x, int y, wchar_t *text, int len,
+    unsigned long attrs, int line_attrs, truecolour tc)
+{ win->vt->draw_cursor(win, x, y, text, len, attrs, line_attrs, tc); }
+static inline void win_draw_trust_sigil(TermWin *win, int x, int y)
+{ win->vt->draw_trust_sigil(win, x, y); }
+static inline int win_char_width(TermWin *win, int uc)
+{ return win->vt->char_width(win, uc); }
+static inline void win_free_draw_ctx(TermWin *win)
+{ win->vt->free_draw_ctx(win); }
+static inline void win_set_cursor_pos(TermWin *win, int x, int y)
+{ win->vt->set_cursor_pos(win, x, y); }
+static inline void win_set_raw_mouse_mode(TermWin *win, bool enable)
+{ win->vt->set_raw_mouse_mode(win, enable); }
+static inline void win_set_raw_mouse_mode_pointer(TermWin *win, bool enable)
+{ win->vt->set_raw_mouse_mode_pointer(win, enable); }
+static inline void win_set_scrollbar(TermWin *win, int t, int s, int p)
+{ win->vt->set_scrollbar(win, t, s, p); }
+static inline void win_bell(TermWin *win, int mode)
+{ win->vt->bell(win, mode); }
+static inline void win_clip_write(
+    TermWin *win, int clipboard, wchar_t *text, int *attrs,
+    truecolour *colours, int len, bool deselect)
+{ win->vt->clip_write(win, clipboard, text, attrs, colours, len, deselect); }
+static inline void win_clip_request_paste(TermWin *win, int clipboard)
+{ win->vt->clip_request_paste(win, clipboard); }
+static inline void win_refresh(TermWin *win)
+{ win->vt->refresh(win); }
+static inline void win_request_resize(TermWin *win, int w, int h)
+{ win->vt->request_resize(win, w, h); }
+static inline void win_set_title(TermWin *win, const char *title, int codepage)
+{ win->vt->set_title(win, title, codepage); }
+static inline void win_set_icon_title(TermWin *win, const char *icontitle,
+                                      int codepage)
+{ win->vt->set_icon_title(win, icontitle, codepage); }
+static inline void win_set_minimised(TermWin *win, bool minimised)
+{ win->vt->set_minimised(win, minimised); }
+static inline void win_set_maximised(TermWin *win, bool maximised)
+{ win->vt->set_maximised(win, maximised); }
+static inline void win_move(TermWin *win, int x, int y)
+{ win->vt->move(win, x, y); }
+static inline void win_set_zorder(TermWin *win, bool top)
+{ win->vt->set_zorder(win, top); }
+static inline void win_palette_set(
+    TermWin *win, unsigned start, unsigned ncolours, const rgb *colours)
+{ win->vt->palette_set(win, start, ncolours, colours); }
+static inline void win_palette_get_overrides(TermWin *win, Terminal *term)
+{ win->vt->palette_get_overrides(win, term); }
+static inline void win_unthrottle(TermWin *win, size_t size)
+{ win->vt->unthrottle(win, size); }
+
+/*
+ * Global functions not specific to a connection instance.
+ */
+void nonfatal(const char *, ...) PRINTF_LIKE(1, 2);
+NORETURN void modalfatalbox(const char *, ...) PRINTF_LIKE(1, 2);
+NORETURN void cleanup_exit(int);
+
+/*
+ * Exports from conf.c, and a big enum (via parametric macro) of
+ * configuration option keys.
+ */
+#define CONFIG_OPTIONS(X) \
+    /* X(value-type, subkey-type, keyword) */ \
+    X(STR, NONE, host) \
+    X(INT, NONE, port) \
+    X(INT, NONE, protocol) /* PROT_SSH, PROT_TELNET etc */ \
+    X(INT, NONE, addressfamily) /* ADDRTYPE_IPV[46] or ADDRTYPE_UNSPEC */ \
+    X(INT, NONE, close_on_exit) /* FORCE_ON, FORCE_OFF, AUTO */ \
+    X(BOOL, NONE, warn_on_close) \
+    X(INT, NONE, ping_interval) /* in seconds */ \
+    X(BOOL, NONE, tcp_nodelay) \
+    X(BOOL, NONE, tcp_keepalives) \
+    X(STR, NONE, loghost) /* logical host being contacted, for host key check */ \
+    /* Proxy options */ \
+    X(STR, NONE, proxy_exclude_list) \
+    X(INT, NONE, proxy_dns) /* FORCE_ON, FORCE_OFF, AUTO */ \
+    X(BOOL, NONE, even_proxy_localhost) \
+    X(INT, NONE, proxy_type) /* PROXY_NONE, PROXY_SOCKS4, ... */ \
+    X(STR, NONE, proxy_host) \
+    X(INT, NONE, proxy_port) \
+    X(STR, NONE, proxy_username) \
+    X(STR, NONE, proxy_password) \
+    X(STR, NONE, proxy_telnet_command) \
+    X(INT, NONE, proxy_log_to_term) /* FORCE_ON, FORCE_OFF, AUTO */ \
+    /* SSH options */ \
+    X(STR, NONE, remote_cmd) \
+    X(STR, NONE, remote_cmd2) /* fallback if remote_cmd fails; never loaded or saved */ \
+    X(BOOL, NONE, nopty) \
+    X(BOOL, NONE, compression) \
+    X(INT, INT, ssh_kexlist) \
+    X(INT, INT, ssh_hklist) \
+    X(BOOL, NONE, ssh_prefer_known_hostkeys) \
+    X(INT, NONE, ssh_rekey_time) /* in minutes */ \
+    X(STR, NONE, ssh_rekey_data) /* string encoding e.g. "100K", "2M", "1G" */ \
+    X(BOOL, NONE, tryagent) \
+    X(BOOL, NONE, agentfwd) \
+    X(BOOL, NONE, change_username) /* allow username switching in SSH-2 */ \
+    X(INT, INT, ssh_cipherlist) \
+    X(FILENAME, NONE, keyfile) \
+    X(FILENAME, NONE, detached_cert) \
+    X(STR, NONE, auth_plugin) \
+    /* \
+     * Which SSH protocol to use. \
+     * For historical reasons, the current legal values for CONF_sshprot \
+     * are: \
+     *  0 = SSH-1 only \
+     *  3 = SSH-2 only \
+     * We used to also support \
+     *  1 = SSH-1 with fallback to SSH-2 \
+     *  2 = SSH-2 with fallback to SSH-1 \
+     * and we continue to use 0/3 in storage formats rather than the more \
+     * obvious 1/2 to avoid surprises if someone saves a session and later \
+     * downgrades PuTTY. So it's easier to use these numbers internally too. \
+     */ \
+    X(INT, NONE, sshprot) \
+    X(BOOL, NONE, ssh2_des_cbc) /* "des-cbc" unrecommended SSH-2 cipher */ \
+    X(BOOL, NONE, ssh_no_userauth) /* bypass "ssh-userauth" (SSH-2 only) */ \
+    X(BOOL, NONE, ssh_no_trivial_userauth) /* disable trivial types of auth */ \
+    X(BOOL, NONE, ssh_show_banner) /* show USERAUTH_BANNERs (SSH-2 only) */ \
+    X(BOOL, NONE, try_tis_auth) \
+    X(BOOL, NONE, try_ki_auth) \
+    X(BOOL, NONE, try_gssapi_auth) /* attempt gssapi auth via ssh userauth */ \
+    X(BOOL, NONE, try_gssapi_kex) /* attempt gssapi auth via ssh kex */ \
+    X(BOOL, NONE, gssapifwd) /* forward tgt via gss */ \
+    X(INT, NONE, gssapirekey) /* KEXGSS refresh interval (mins) */ \
+    X(INT, INT, ssh_gsslist) /* preference order for local GSS libs */ \
+    X(FILENAME, NONE, ssh_gss_custom) \
+    X(BOOL, NONE, ssh_subsys) /* run a subsystem rather than a command */ \
+    X(BOOL, NONE, ssh_subsys2) /* fallback to go with remote_cmd_ptr2 */ \
+    X(BOOL, NONE, ssh_no_shell) /* avoid running a shell */ \
+    X(STR, NONE, ssh_nc_host) /* host to connect to in `nc' mode */ \
+    X(INT, NONE, ssh_nc_port) /* port to connect to in `nc' mode */ \
+    /* Telnet options */ \
+    X(STR, NONE, termtype) \
+    X(STR, NONE, termspeed) \
+    X(STR, STR, ttymodes) /* values are "Vvalue" or "A" */ \
+    X(STR, STR, environmt) \
+    X(STR, NONE, username) \
+    X(BOOL, NONE, username_from_env) \
+    X(STR, NONE, localusername) \
+    X(BOOL, NONE, rfc_environ) \
+    X(BOOL, NONE, passive_telnet) \
+    /* Serial port options */ \
+    X(STR, NONE, serline) \
+    X(INT, NONE, serspeed) \
+    X(INT, NONE, serdatabits) \
+    X(INT, NONE, serstopbits) \
+    X(INT, NONE, serparity) /* SER_PAR_NONE, SER_PAR_ODD, ... */ \
+    X(INT, NONE, serflow) /* SER_FLOW_NONE, SER_FLOW_XONXOFF, ... */ \
+    /* Supdup options */ \
+    X(STR, NONE, supdup_location) \
+    X(INT, NONE, supdup_ascii_set) \
+    X(BOOL, NONE, supdup_more) \
+    X(BOOL, NONE, supdup_scroll) \
+    /* Keyboard options */ \
+    X(BOOL, NONE, bksp_is_delete) \
+    X(BOOL, NONE, rxvt_homeend) \
+    X(INT, NONE, funky_type) /* FUNKY_XTERM, FUNKY_LINUX, ... */ \
+    X(INT, NONE, sharrow_type) /* SHARROW_APPLICATION, SHARROW_BITMAP, ... */ \
+    X(BOOL, NONE, no_applic_c) /* totally disable app cursor keys */ \
+    X(BOOL, NONE, no_applic_k) /* totally disable app keypad */ \
+    X(BOOL, NONE, no_mouse_rep) /* totally disable mouse reporting */ \
+    X(BOOL, NONE, no_remote_resize) /* disable remote resizing */ \
+    X(BOOL, NONE, no_alt_screen) /* disable alternate screen */ \
+    X(BOOL, NONE, no_remote_wintitle) /* disable remote retitling */ \
+    X(BOOL, NONE, no_remote_clearscroll) /* disable ESC[3J */ \
+    X(BOOL, NONE, no_dbackspace) /* disable destructive backspace */ \
+    X(BOOL, NONE, no_remote_charset) /* disable remote charset config */ \
+    X(INT, NONE, remote_qtitle_action) /* remote win title query action
+                                       * (TITLE_NONE, TITLE_EMPTY, ...) */ \
+    X(BOOL, NONE, app_cursor) \
+    X(BOOL, NONE, app_keypad) \
+    X(BOOL, NONE, nethack_keypad) \
+    X(BOOL, NONE, telnet_keyboard) \
+    X(BOOL, NONE, telnet_newline) \
+    X(BOOL, NONE, alt_f4) /* is it special? */ \
+    X(BOOL, NONE, alt_space) /* is it special? */ \
+    X(BOOL, NONE, alt_only) /* is it special? */ \
+    X(INT, NONE, localecho) /* FORCE_ON, FORCE_OFF, AUTO */ \
+    X(INT, NONE, localedit) /* FORCE_ON, FORCE_OFF, AUTO */ \
+    X(BOOL, NONE, alwaysontop) \
+    X(BOOL, NONE, fullscreenonaltenter) \
+    X(BOOL, NONE, scroll_on_key) \
+    X(BOOL, NONE, scroll_on_disp) \
+    X(BOOL, NONE, erase_to_scrollback) \
+    X(BOOL, NONE, compose_key) \
+    X(BOOL, NONE, ctrlaltkeys) \
+    X(BOOL, NONE, osx_option_meta) \
+    X(BOOL, NONE, osx_command_meta) \
+    X(STR, NONE, wintitle) /* initial window title */ \
+    /* Terminal options */ \
+    X(INT, NONE, savelines) \
+    X(BOOL, NONE, dec_om) \
+    X(BOOL, NONE, wrap_mode) \
+    X(BOOL, NONE, lfhascr) \
+    X(INT, NONE, cursor_type) /* 0=block 1=underline 2=vertical */ \
+    X(BOOL, NONE, blink_cur) \
+    X(INT, NONE, beep) /* BELL_DISABLED, BELL_DEFAULT, ... */ \
+    X(INT, NONE, beep_ind) /* B_IND_DISABLED, B_IND_FLASH, ... */ \
+    X(BOOL, NONE, bellovl) /* bell overload protection active? */ \
+    X(INT, NONE, bellovl_n) /* number of bells to cause overload */ \
+    X(INT, NONE, bellovl_t) /* time interval for overload (seconds) */ \
+    X(INT, NONE, bellovl_s) /* period of silence to re-enable bell (s) */ \
+    X(FILENAME, NONE, bell_wavefile) \
+    X(BOOL, NONE, scrollbar) \
+    X(BOOL, NONE, scrollbar_in_fullscreen) \
+    X(INT, NONE, resize_action) /* RESIZE_TERM, RESIZE_DISABLED, ... */ \
+    X(BOOL, NONE, bce) \
+    X(BOOL, NONE, blinktext) \
+    X(BOOL, NONE, win_name_always) \
+    X(INT, NONE, width) \
+    X(INT, NONE, height) \
+    X(FONT, NONE, font) \
+    X(INT, NONE, font_quality) /* FQ_DEFAULT, FQ_ANTIALIASED, ... */ \
+    X(FILENAME, NONE, logfilename) \
+    X(INT, NONE, logtype) /* LGTYP_NONE, LGTYPE_ASCII, ... */ \
+    X(INT, NONE, logxfovr) /* LGXF_OVR, LGXF_APN, LGXF_ASK */ \
+    X(BOOL, NONE, logflush) \
+    X(BOOL, NONE, logheader) \
+    X(BOOL, NONE, logomitpass) \
+    X(BOOL, NONE, logomitdata) \
+    X(BOOL, NONE, hide_mouseptr) \
+    X(BOOL, NONE, sunken_edge) \
+    X(INT, NONE, window_border) /* in pixels */ \
+    X(STR, NONE, answerback) \
+    X(STR, NONE, printer) \
+    X(BOOL, NONE, no_arabicshaping) \
+    X(BOOL, NONE, no_bidi) \
+    /* Colour options */ \
+    X(BOOL, NONE, ansi_colour) \
+    X(BOOL, NONE, xterm_256_colour) \
+    X(BOOL, NONE, true_colour) \
+    X(BOOL, NONE, system_colour) \
+    X(BOOL, NONE, try_palette) \
+    X(INT, NONE, bold_style) /* 1=font 2=colour (3=both) */ \
+    X(INT, INT, colours) /* indexed by the CONF_COLOUR_* enum encoding */ \
+    /* Selection options */ \
+    X(INT, NONE, mouse_is_xterm) /* 0=compromise 1=xterm 2=Windows */ \
+    X(BOOL, NONE, rect_select) \
+    X(BOOL, NONE, paste_controls) \
+    X(BOOL, NONE, rawcnp) \
+    X(BOOL, NONE, utf8linedraw) \
+    X(BOOL, NONE, rtf_paste) \
+    X(BOOL, NONE, mouse_override) \
+    X(INT, INT, wordness) \
+    X(BOOL, NONE, mouseautocopy) \
+    X(INT, NONE, mousepaste) /* CLIPUI_IMPLICIT, CLIPUI_EXPLICIT, ... */ \
+    X(INT, NONE, ctrlshiftins) /* CLIPUI_IMPLICIT, CLIPUI_EXPLICIT, ... */ \
+    X(INT, NONE, ctrlshiftcv) /* CLIPUI_IMPLICIT, CLIPUI_EXPLICIT, ... */ \
+    X(STR, NONE, mousepaste_custom) \
+    X(STR, NONE, ctrlshiftins_custom) \
+    X(STR, NONE, ctrlshiftcv_custom) \
+    /* translations */ \
+    X(INT, NONE, vtmode) /* VT_XWINDOWS, VT_OEMANSI, ... */ \
+    X(STR, NONE, line_codepage) \
+    X(BOOL, NONE, cjk_ambig_wide) \
+    X(BOOL, NONE, utf8_override) \
+    X(BOOL, NONE, xlat_capslockcyr) \
+    /* X11 forwarding */ \
+    X(BOOL, NONE, x11_forward) \
+    X(STR, NONE, x11_display) \
+    X(INT, NONE, x11_auth) /* X11_NO_AUTH, X11_MIT, X11_XDM */ \
+    X(FILENAME, NONE, xauthfile) \
+    /* port forwarding */ \
+    X(BOOL, NONE, lport_acceptall) /* accept conns from hosts other than localhost */ \
+    X(BOOL, NONE, rport_acceptall) /* same for remote forwarded ports (SSH-2 only) */ \
+    /*                                                                \
+     * Subkeys for 'portfwd' can have the following forms:            \
+     *                                                                \
+     *   [LR]localport                                                \
+     *   [LR]localaddr:localport                                      \
+     *                                                                \
+     * Dynamic forwardings are indicated by an 'L' key, and the       \
+     * special value "D". For all other forwardings, the value        \
+     * should be of the form 'host:port'.                             \
+     */ \
+    X(STR, STR, portfwd) \
+    /* SSH bug compatibility modes. All FORCE_ON/FORCE_OFF/AUTO */ \
+    X(INT, NONE, sshbug_ignore1) \
+    X(INT, NONE, sshbug_plainpw1) \
+    X(INT, NONE, sshbug_rsa1) \
+    X(INT, NONE, sshbug_hmac2) \
+    X(INT, NONE, sshbug_derivekey2) \
+    X(INT, NONE, sshbug_rsapad2) \
+    X(INT, NONE, sshbug_pksessid2) \
+    X(INT, NONE, sshbug_rekey2) \
+    X(INT, NONE, sshbug_maxpkt2) \
+    X(INT, NONE, sshbug_ignore2) \
+    X(INT, NONE, sshbug_oldgex2) \
+    X(INT, NONE, sshbug_winadj) \
+    X(INT, NONE, sshbug_chanreq) \
+    X(INT, NONE, sshbug_dropstart) \
+    X(INT, NONE, sshbug_filter_kexinit) \
+    X(INT, NONE, sshbug_rsa_sha2_cert_userauth) \
+    /*                                                                \
+     * ssh_simple means that we promise never to open any channel     \
+     * other than the main one, which means it can safely use a very  \
+     * large window in SSH-2.                                         \
+     */ \
+    X(BOOL, NONE, ssh_simple) \
+    X(BOOL, NONE, ssh_connection_sharing) \
+    X(BOOL, NONE, ssh_connection_sharing_upstream) \
+    X(BOOL, NONE, ssh_connection_sharing_downstream) \
+    /*
+     * ssh_manual_hostkeys is conceptually a set rather than a
+     * dictionary: the string subkeys are the important thing, and the
+     * actual values to which those subkeys map are all "".
+     */ \
+    X(STR, STR, ssh_manual_hostkeys) \
+    /* Options for pterm. Should split out into platform-dependent part. */ \
+    X(BOOL, NONE, stamp_utmp) \
+    X(BOOL, NONE, login_shell) \
+    X(BOOL, NONE, scrollbar_on_left) \
+    X(BOOL, NONE, shadowbold) \
+    X(FONT, NONE, boldfont) \
+    X(FONT, NONE, widefont) \
+    X(FONT, NONE, wideboldfont) \
+    X(INT, NONE, shadowboldoffset) /* in pixels */ \
+    X(BOOL, NONE, crhaslf) \
+    X(STR, NONE, winclass) \
+    /* end of list */
+
+/* Now define the actual enum of option keywords using that macro. */
+#define CONF_ENUM_DEF(valtype, keytype, keyword) CONF_ ## keyword,
+enum config_primary_key { CONFIG_OPTIONS(CONF_ENUM_DEF) N_CONFIG_OPTIONS };
+#undef CONF_ENUM_DEF
+
+/* Functions handling configuration structures. */
+Conf *conf_new(void);                  /* create an empty configuration */
+void conf_free(Conf *conf);
+Conf *conf_copy(Conf *oldconf);
+void conf_copy_into(Conf *dest, Conf *src);
+/* Mandatory accessor functions: enforce by assertion that keys exist. */
+bool conf_get_bool(Conf *conf, int key);
+int conf_get_int(Conf *conf, int key);
+int conf_get_int_int(Conf *conf, int key, int subkey);
+char *conf_get_str(Conf *conf, int key);   /* result still owned by conf */
+char *conf_get_str_str(Conf *conf, int key, const char *subkey);
+Filename *conf_get_filename(Conf *conf, int key);
+FontSpec *conf_get_fontspec(Conf *conf, int key); /* still owned by conf */
+/* Optional accessor function: return NULL if key does not exist. */
+char *conf_get_str_str_opt(Conf *conf, int key, const char *subkey);
+/* Accessor function to step through a string-subkeyed list.
+ * Returns the next subkey after the provided one, or the first if NULL.
+ * Returns NULL if there are none left.
+ * Both the return value and *subkeyout are still owned by conf. */
+char *conf_get_str_strs(Conf *conf, int key, char *subkeyin, char **subkeyout);
+/* Return the nth string subkey in a list. Owned by conf. NULL if beyond end */
+char *conf_get_str_nthstrkey(Conf *conf, int key, int n);
+/* Functions to set entries in configuration. Always copy their inputs. */
+void conf_set_bool(Conf *conf, int key, bool value);
+void conf_set_int(Conf *conf, int key, int value);
+void conf_set_int_int(Conf *conf, int key, int subkey, int value);
+void conf_set_str(Conf *conf, int key, const char *value);
+void conf_set_str_str(Conf *conf, int key,
+                      const char *subkey, const char *val);
+void conf_del_str_str(Conf *conf, int key, const char *subkey);
+void conf_set_filename(Conf *conf, int key, const Filename *val);
+void conf_set_fontspec(Conf *conf, int key, const FontSpec *val);
+/* Serialisation functions for Duplicate Session */
+void conf_serialise(BinarySink *bs, Conf *conf);
+bool conf_deserialise(Conf *conf, BinarySource *src);/*returns true on success*/
+
+/*
+ * Functions to copy, free, serialise and deserialise FontSpecs.
+ * Provided per-platform, to go with the platform's idea of a
+ * FontSpec's contents.
+ */
+FontSpec *fontspec_copy(const FontSpec *f);
+void fontspec_free(FontSpec *f);
+void fontspec_serialise(BinarySink *bs, FontSpec *f);
+FontSpec *fontspec_deserialise(BinarySource *src);
+
+/*
+ * Exports from each platform's noise.c.
+ */
+typedef enum NoiseSourceId {
+    NOISE_SOURCE_TIME,
+    NOISE_SOURCE_IOID,
+    NOISE_SOURCE_IOLEN,
+    NOISE_SOURCE_KEY,
+    NOISE_SOURCE_MOUSEBUTTON,
+    NOISE_SOURCE_MOUSEPOS,
+    NOISE_SOURCE_MEMINFO,
+    NOISE_SOURCE_STAT,
+    NOISE_SOURCE_RUSAGE,
+    NOISE_SOURCE_FGWINDOW,
+    NOISE_SOURCE_CAPTURE,
+    NOISE_SOURCE_CLIPBOARD,
+    NOISE_SOURCE_QUEUE,
+    NOISE_SOURCE_CURSORPOS,
+    NOISE_SOURCE_THREADTIME,
+    NOISE_SOURCE_PROCTIME,
+    NOISE_SOURCE_PERFCOUNT,
+    NOISE_MAX_SOURCES
+} NoiseSourceId;
+void noise_get_heavy(void (*func) (void *, int));
+void noise_get_light(void (*func) (void *, int));
+void noise_regular(void);
+void noise_ultralight(NoiseSourceId id, unsigned long data);
+
+/*
+ * Exports from sshrand.c.
+ */
+void random_save_seed(void);
+void random_destroy_seed(void);
+
+/*
+ * Exports from settings.c.
+ *
+ * load_settings() and do_defaults() return false if the provided
+ * session name didn't actually exist. But they still fill in the
+ * provided Conf with _something_.
+ */
+const struct BackendVtable *backend_vt_from_name(const char *name);
+const struct BackendVtable *backend_vt_from_proto(int proto);
+char *get_remote_username(Conf *conf); /* dynamically allocated */
+char *save_settings(const char *section, Conf *conf);
+void save_open_settings(settings_w *sesskey, Conf *conf);
+bool load_settings(const char *section, Conf *conf);
+void load_open_settings(settings_r *sesskey, Conf *conf);
+void get_sesslist(struct sesslist *, bool allocate);
+bool do_defaults(const char *, Conf *);
+void registry_cleanup(void);
+void settings_set_default_protocol(int);
+void settings_set_default_port(int);
+
+/*
+ * Functions used by settings.c to provide platform-specific
+ * default settings.
+ *
+ * (The integer one is expected to return `def' if it has no clear
+ * opinion of its own. This is because there's no integer value
+ * which I can reliably set aside to indicate `nil'. The string
+ * function is perfectly all right returning NULL, of course. The
+ * Filename and FontSpec functions are _not allowed_ to fail to
+ * return, since these defaults _must_ be per-platform.)
+ *
+ * The 'Filename *' returned by platform_default_filename, and the
+ * 'FontSpec *' returned by platform_default_fontspec, have ownership
+ * transferred to the caller, and must be freed.
+ */
+char *platform_default_s(const char *name);
+bool platform_default_b(const char *name, bool def);
+int platform_default_i(const char *name, int def);
+Filename *platform_default_filename(const char *name);
+FontSpec *platform_default_fontspec(const char *name);
+
+/*
+ * Exports from terminal.c.
+ */
+
+Terminal *term_init(Conf *, struct unicode_data *, TermWin *);
+void term_free(Terminal *);
+void term_size(Terminal *, int, int, int);
+void term_resize_request_completed(Terminal *);
+void term_paint(Terminal *, int, int, int, int, bool);
+void term_scroll(Terminal *, int, int);
+void term_scroll_to_selection(Terminal *, int);
+void term_pwron(Terminal *, bool);
+void term_clrsb(Terminal *);
+void term_mouse(Terminal *, Mouse_Button, Mouse_Button, Mouse_Action,
+                int, int, bool, bool, bool);
+void term_cancel_selection_drag(Terminal *);
+void term_key(Terminal *, Key_Sym, wchar_t *, size_t, unsigned int,
+              unsigned int);
+void term_lost_clipboard_ownership(Terminal *, int clipboard);
+void term_update(Terminal *);
+void term_invalidate(Terminal *);
+void term_blink(Terminal *, bool set_cursor);
+void term_do_paste(Terminal *, const wchar_t *, int);
+void term_nopaste(Terminal *);
+void term_copyall(Terminal *, const int *, int);
+void term_pre_reconfig(Terminal *, Conf *);
+void term_reconfig(Terminal *, Conf *);
+void term_request_copy(Terminal *, const int *clipboards, int n_clipboards);
+void term_request_paste(Terminal *, int clipboard);
+void term_seen_key_event(Terminal *);
+size_t term_data(Terminal *, const void *data, size_t len);
+void term_provide_backend(Terminal *term, Backend *backend);
+void term_provide_logctx(Terminal *term, LogContext *logctx);
+void term_set_focus(Terminal *term, bool has_focus);
+char *term_get_ttymode(Terminal *term, const char *mode);
+SeatPromptResult term_get_userpass_input(Terminal *term, prompts_t *p);
+void term_set_trust_status(Terminal *term, bool trusted);
+void term_keyinput(Terminal *, int codepage, const void *buf, int len);
+void term_keyinputw(Terminal *, const wchar_t *widebuf, int len);
+void term_get_cursor_position(Terminal *term, int *x, int *y);
+void term_setup_window_titles(Terminal *term, const char *title_hostname);
+void term_notify_minimised(Terminal *term, bool minimised);
+void term_notify_palette_changed(Terminal *term);
+void term_notify_window_pos(Terminal *term, int x, int y);
+void term_notify_window_size_pixels(Terminal *term, int x, int y);
+void term_palette_override(Terminal *term, unsigned osc4_index, rgb rgb);
+
+typedef enum SmallKeypadKey {
+    SKK_HOME, SKK_END, SKK_INSERT, SKK_DELETE, SKK_PGUP, SKK_PGDN,
+} SmallKeypadKey;
+int format_arrow_key(char *buf, Terminal *term, int xkey,
+                     bool shift, bool ctrl, bool alt, bool *consumed_alt);
+int format_function_key(char *buf, Terminal *term, int key_number,
+                        bool shift, bool ctrl, bool alt, bool *consumed_alt);
+int format_small_keypad_key(char *buf, Terminal *term, SmallKeypadKey key,
+                            bool shift, bool ctrl, bool alt,
+                            bool *consumed_alt);
+int format_numeric_keypad_key(char *buf, Terminal *term, char key,
+                              bool shift, bool ctrl);
+
+/*
+ * Exports from logging.c.
+ */
+struct LogPolicyVtable {
+    /*
+     * Pass Event Log entries on from LogContext to the front end,
+     * which might write them to standard error or save them for a GUI
+     * list box or other things.
+     */
+    void (*eventlog)(LogPolicy *lp, const char *event);
+
+    /*
+     * Ask what to do about the specified output log file already
+     * existing. Can return four values:
+     *
+     *  - 2 means overwrite the log file
+     *  - 1 means append to the log file
+     *  - 0 means cancel logging for this session
+     *  - -1 means please wait, and callback() will be called with one
+     *    of those options.
+     */
+    int (*askappend)(LogPolicy *lp, Filename *filename,
+                     void (*callback)(void *ctx, int result), void *ctx);
+
+    /*
+     * Emergency logging when the log file itself can't be opened,
+     * which typically means we want to shout about it more loudly
+     * than a mere Event Log entry.
+     *
+     * One reasonable option is to send it to the same place that
+     * stderr output from the main session goes (so, either a console
+     * tool's actual stderr, or a terminal window). In many cases this
+     * is unlikely to cause this error message to turn up
+     * embarrassingly in a log file of real server output, because the
+     * whole point is that we haven't managed to open any such log
+     * file :-)
+     */
+    void (*logging_error)(LogPolicy *lp, const char *event);
+
+    /*
+     * Ask whether extra verbose log messages are required.
+     */
+    bool (*verbose)(LogPolicy *lp);
+};
+struct LogPolicy {
+    const LogPolicyVtable *vt;
+};
+
+static inline void lp_eventlog(LogPolicy *lp, const char *event)
+{ lp->vt->eventlog(lp, event); }
+static inline int lp_askappend(
+    LogPolicy *lp, Filename *filename,
+    void (*callback)(void *ctx, int result), void *ctx)
+{ return lp->vt->askappend(lp, filename, callback, ctx); }
+static inline void lp_logging_error(LogPolicy *lp, const char *event)
+{ lp->vt->logging_error(lp, event); }
+static inline bool lp_verbose(LogPolicy *lp)
+{ return lp->vt->verbose(lp); }
+
+/* Defined in clicons.c, used in several console command-line tools */
+extern LogPolicy console_cli_logpolicy[];
+
+int console_askappend(LogPolicy *lp, Filename *filename,
+                      void (*callback)(void *ctx, int result), void *ctx);
+void console_logging_error(LogPolicy *lp, const char *string);
+void console_eventlog(LogPolicy *lp, const char *string);
+bool null_lp_verbose_yes(LogPolicy *lp);
+bool null_lp_verbose_no(LogPolicy *lp);
+bool cmdline_lp_verbose(LogPolicy *lp);
+
+LogContext *log_init(LogPolicy *lp, Conf *conf);
+void log_free(LogContext *logctx);
+void log_reconfig(LogContext *logctx, Conf *conf);
+void logfopen(LogContext *logctx);
+void logfclose(LogContext *logctx);
+void logtraffic(LogContext *logctx, unsigned char c, int logmode);
+void logflush(LogContext *logctx);
+LogPolicy *log_get_policy(LogContext *logctx);
+void logevent(LogContext *logctx, const char *event);
+void logeventf(LogContext *logctx, const char *fmt, ...) PRINTF_LIKE(2, 3);
+void logeventvf(LogContext *logctx, const char *fmt, va_list ap);
+
+/*
+ * Pass a dynamically allocated string to logevent and immediately
+ * free it. Intended for use by wrapper macros which pass the return
+ * value of dupprintf straight to this.
+ */
+void logevent_and_free(LogContext *logctx, char *event);
+enum { PKT_INCOMING, PKT_OUTGOING };
+enum { PKTLOG_EMIT, PKTLOG_BLANK, PKTLOG_OMIT };
+struct logblank_t {
+    int offset;
+    int len;
+    int type;
+};
+void log_packet(LogContext *logctx, int direction, int type,
+                const char *texttype, const void *data, size_t len,
+                int n_blanks, const struct logblank_t *blanks,
+                const unsigned long *sequence,
+                unsigned downstream_id, const char *additional_log_text);
+
+/*
+ * Exports from testback.c
+ */
+
+extern const struct BackendVtable null_backend;
+extern const struct BackendVtable loop_backend;
+
+/*
+ * Exports from raw.c.
+ */
+
+extern const struct BackendVtable raw_backend;
+
+/*
+ * Exports from rlogin.c.
+ */
+
+extern const struct BackendVtable rlogin_backend;
+
+/*
+ * Exports from telnet.c.
+ */
+
+extern const struct BackendVtable telnet_backend;
+
+/*
+ * Exports from ssh/ssh.c.
+ */
+extern const struct BackendVtable ssh_backend;
+extern const struct BackendVtable sshconn_backend;
+
+/*
+ * Exports from supdup.c.
+ */
+extern const struct BackendVtable supdup_backend;
+
+/*
+ * Exports from ldisc.c.
+ */
+Ldisc *ldisc_create(Conf *, Terminal *, Backend *, Seat *);
+void ldisc_configure(Ldisc *, Conf *);
+void ldisc_free(Ldisc *);
+void ldisc_send(Ldisc *, const void *buf, int len, bool interactive);
+void ldisc_echoedit_update(Ldisc *);
+typedef struct LdiscInputToken {
+    /*
+     * Structure that encodes any single item of data that Ldisc can
+     * buffer: either a single character of raw data, or a session
+     * special.
+     */
+    bool is_special;
+    union {
+        struct {
+            /* if is_special == false */
+            char chr;
+        };
+        struct {
+            /* if is_special == true */
+            SessionSpecialCode code;
+            int arg;
+        };
+    };
+} LdiscInputToken;
+bool ldisc_has_input_buffered(Ldisc *);
+LdiscInputToken ldisc_get_input_token(Ldisc *); /* asserts there is input */
+void ldisc_enable_prompt_callback(Ldisc *, prompts_t *);
+void ldisc_check_sendok(Ldisc *);
+
+/*
+ * Exports from sshrand.c.
+ */
+
+void random_add_noise(NoiseSourceId source, const void *noise, int length);
+void random_read(void *buf, size_t size);
+void random_get_savedata(void **data, int *len);
+extern int random_active;
+/* The random number subsystem is activated if at least one other entity
+ * within the program expresses an interest in it. So each SSH session
+ * calls random_ref on startup and random_unref on shutdown. */
+void random_ref(void);
+void random_unref(void);
+/* random_clear is equivalent to calling random_unref as many times as
+ * necessary to shut down the global PRNG instance completely. It's
+ * not needed in normal applications, but the command-line PuTTYgen
+ * test finds it useful to clean up after each invocation of the
+ * logical main() no matter whether it needed random numbers or
+ * not. */
+void random_clear(void);
+/* random_setup_custom sets up the process-global random number
+ * generator specially, with a hash function of your choice. */
+void random_setup_custom(const ssh_hashalg *hash);
+/* random_setup_special() is a macro wrapper on that, which makes an
+ * extra-big one based on the largest hash function we have. It's
+ * defined this way to avoid what would otherwise be an unnecessary
+ * module dependency from sshrand.c to a hash function implementation. */
+#define random_setup_special() random_setup_custom(&ssh_shake256_114bytes)
+/* Manually drop a random seed into the random number generator, e.g.
+ * just before generating a key. */
+void random_reseed(ptrlen seed);
+/* Limit on how much entropy is worth putting into the generator (bits). */
+size_t random_seed_bits(void);
+
+/*
+ * Exports from pinger.c.
+ */
+typedef struct Pinger Pinger;
+Pinger *pinger_new(Conf *conf, Backend *backend);
+void pinger_reconfig(Pinger *, Conf *oldconf, Conf *newconf);
+void pinger_free(Pinger *);
+
+/*
+ * Exports from modules in utils.
+ */
+
+#include "misc.h"
+bool conf_launchable(Conf *conf);
+char const *conf_dest(Conf *conf);
+
+/*
+ * Exports from sessprep.c.
+ */
+void prepare_session(Conf *conf);
+
+/*
+ * Exports from version.c and cmake_commit.c.
+ */
+extern const char ver[];
+extern const char commitid[];
+
+/*
+ * Exports from unicode.c in platform subdirs.
+ */
+#ifndef CP_UTF8
+#define CP_UTF8 65001
+#endif
+/* void init_ucs(void); -- this is now in platform-specific headers */
+bool is_dbcs_leadbyte(int codepage, char byte);
+int mb_to_wc(int codepage, int flags, const char *mbstr, int mblen,
+             wchar_t *wcstr, int wclen);
+int wc_to_mb(int codepage, int flags, const wchar_t *wcstr, int wclen,
+             char *mbstr, int mblen, const char *defchr);
+wchar_t xlat_uskbd2cyrllic(int ch);
+int check_compose(int first, int second);
+int decode_codepage(const char *cp_name);
+const char *cp_enumerate (int index);
+const char *cp_name(int codepage);
+void get_unitab(int codepage, wchar_t *unitab, int ftype);
+
+/*
+ * Exports from wcwidth.c
+ */
+int mk_wcwidth(unsigned int ucs);
+int mk_wcswidth(const unsigned int *pwcs, size_t n);
+int mk_wcwidth_cjk(unsigned int ucs);
+int mk_wcswidth_cjk(const unsigned int *pwcs, size_t n);
+
+/*
+ * Exports from agent-client.c in platform subdirs.
+ *
+ * agent_query returns NULL for here's-a-response, and non-NULL for
+ * query-in- progress. In the latter case there will be a call to
+ * `callback' at some future point, passing callback_ctx as the first
+ * parameter and the actual reply data as the second and third.
+ *
+ * The response may be a NULL pointer (in either of the synchronous
+ * or asynchronous cases), which indicates failure to receive a
+ * response.
+ *
+ * When the return from agent_query is not NULL, it identifies the
+ * in-progress query in case it needs to be cancelled. If
+ * agent_cancel_query is called, then the pending query is destroyed
+ * and the callback will not be called. (E.g. if you're going to throw
+ * away the thing you were using as callback_ctx.)
+ *
+ * Passing a null pointer as callback forces agent_query to behave
+ * synchronously, i.e. it will block if necessary, and guarantee to
+ * return NULL. The wrapper function agent_query_synchronous()
+ * (defined in its own module aqsync.c) makes this easier.
+ */
+typedef struct agent_pending_query agent_pending_query;
+agent_pending_query *agent_query(
+    strbuf *in, void **out, int *outlen,
+    void (*callback)(void *, void *, int), void *callback_ctx);
+void agent_cancel_query(agent_pending_query *);
+void agent_query_synchronous(strbuf *in, void **out, int *outlen);
+bool agent_exists(void);
+
+/* For stream-oriented agent connections, if available. */
+Socket *agent_connect(Plug *plug);
+
+/*
+ * Exports from wildcard.c
+ */
+const char *wc_error(int value);
+int wc_match_pl(const char *wildcard, ptrlen target);
+int wc_match(const char *wildcard, const char *target);
+bool wc_unescape(char *output, const char *wildcard);
+
+/*
+ * Exports from frontend (dialog.c etc)
+ */
+void pgp_fingerprints(void);
+/*
+ * have_ssh_host_key() just returns true if a key of that type is
+ * already cached and false otherwise.
+ */
+bool have_ssh_host_key(const char *host, int port, const char *keytype);
+
+/*
+ * Exports from console frontends (console.c in platform subdirs)
+ * that aren't equivalents to things in windlg.c et al.
+ */
+extern bool console_batch_mode, console_antispoof_prompt;
+SeatPromptResult console_get_userpass_input(prompts_t *p);
+bool is_interactive(void);
+void console_print_error_msg(const char *prefix, const char *msg);
+void console_print_error_msg_fmt_v(
+    const char *prefix, const char *fmt, va_list ap);
+void console_print_error_msg_fmt(const char *prefix, const char *fmt, ...)
+    PRINTF_LIKE(2, 3);
+
+/*
+ * Exports from printing.c in platform subdirs.
+ */
+typedef struct printer_enum_tag printer_enum;
+typedef struct printer_job_tag printer_job;
+printer_enum *printer_start_enum(int *nprinters);
+char *printer_get_name(printer_enum *, int);
+void printer_finish_enum(printer_enum *);
+printer_job *printer_start_job(char *printer);
+void printer_job_data(printer_job *, const void *, size_t);
+void printer_finish_job(printer_job *);
+
+/*
+ * Exports from cmdline.c (and also cmdline_error(), which is
+ * defined differently in various places and required _by_
+ * cmdline.c).
+ *
+ * Note that cmdline_process_param takes a const option string, but a
+ * writable argument string. That's not a mistake - that's so it can
+ * zero out password arguments in the hope of not having them show up
+ * avoidably in Unix 'ps'.
+ */
+struct cmdline_get_passwd_input_state { bool tried; };
+#define CMDLINE_GET_PASSWD_INPUT_STATE_INIT { .tried = false }
+extern const cmdline_get_passwd_input_state cmdline_get_passwd_input_state_new;
+
+int cmdline_process_param(const char *, char *, int, Conf *);
+void cmdline_run_saved(Conf *);
+void cmdline_cleanup(void);
+SeatPromptResult cmdline_get_passwd_input(
+    prompts_t *p, cmdline_get_passwd_input_state *state, bool restartable);
+bool cmdline_host_ok(Conf *);
+bool cmdline_verbose(void);
+bool cmdline_loaded_session(void);
+
+/*
+ * Here we have a flags word provided by each tool, which describes
+ * the capabilities of that tool that cmdline.c needs to know about.
+ * It will refuse certain command-line options if a particular tool
+ * inherently can't do anything sensible. For example, the file
+ * transfer tools (psftp, pscp) can't do a great deal with protocol
+ * selections (ever tried running scp over telnet?) or with port
+ * forwarding (even if it wasn't a hideously bad idea, they don't have
+ * the select/poll infrastructure to make them work).
+ */
+extern const unsigned cmdline_tooltype;
+
+/* Bit flags for the above */
+#define TOOLTYPE_LIST(X)                        \
+    X(TOOLTYPE_FILETRANSFER)                    \
+    X(TOOLTYPE_NONNETWORK)                      \
+    X(TOOLTYPE_HOST_ARG)                        \
+    X(TOOLTYPE_HOST_ARG_CAN_BE_SESSION)         \
+    X(TOOLTYPE_HOST_ARG_PROTOCOL_PREFIX)        \
+    X(TOOLTYPE_HOST_ARG_FROM_LAUNCHABLE_LOAD)   \
+    X(TOOLTYPE_PORT_ARG)                        \
+    X(TOOLTYPE_NO_VERBOSE_OPTION)               \
+    /* end of list */
+#define BITFLAG_INDEX(val) val ## _bitflag_index,
+enum { TOOLTYPE_LIST(BITFLAG_INDEX) };
+#define BITFLAG_DEF(val) val = 1U << (val ## _bitflag_index),
+enum { TOOLTYPE_LIST(BITFLAG_DEF) };
+
+void cmdline_error(const char *, ...) PRINTF_LIKE(1, 2);
+
+/*
+ * Exports from config.c.
+ */
+struct controlbox;
+void conf_radiobutton_handler(dlgcontrol *ctrl, dlgparam *dlg,
+                              void *data, int event);
+#define CHECKBOX_INVERT (1<<30)
+void conf_checkbox_handler(dlgcontrol *ctrl, dlgparam *dlg,
+                           void *data, int event);
+void conf_editbox_handler(dlgcontrol *ctrl, dlgparam *dlg,
+                          void *data, int event);
+void conf_filesel_handler(dlgcontrol *ctrl, dlgparam *dlg,
+                          void *data, int event);
+void conf_fontsel_handler(dlgcontrol *ctrl, dlgparam *dlg,
+                          void *data, int event);
+
+struct conf_editbox_handler_type {
+    /* Structure passed as context2 to conf_editbox_handler */
+    enum { EDIT_STR, EDIT_INT, EDIT_FIXEDPOINT } type;
+    union {
+        /*
+         * EDIT_STR means the edit box is connected to a string
+         * field in Conf. No further parameters needed.
+         */
+
+        /*
+         * EDIT_INT means the edit box is connected to an int field in
+         * Conf, and the input string is interpreted as decimal. No
+         * further parameters needed. (But we could add one here later
+         * if for some reason we wanted int fields in hex.)
+         */
+
+        /*
+         * EDIT_FIXEDPOINT means the edit box is connected to an int
+         * field in Conf, but the input string is interpreted as
+         * _floating point_, and converted to/from the output int by
+         * means of a fixed denominator. That is,
+         *
+         *   (floating value in edit box) * denominator = value in Conf
+         */
+        struct {
+            double denominator;
+        };
+    };
+};
+
+extern const struct conf_editbox_handler_type conf_editbox_str;
+extern const struct conf_editbox_handler_type conf_editbox_int;
+#define ED_STR CP(&conf_editbox_str)
+#define ED_INT CP(&conf_editbox_int)
+
+void setup_config_box(struct controlbox *b, bool midsession,
+                      int protocol, int protcfginfo);
+
+void setup_ca_config_box(struct controlbox *b);
+
+/* Platforms provide this to be called from config.c */
+void show_ca_config_box(dlgparam *dlg);
+extern const bool has_ca_config_box; /* false if, e.g., we're PuTTYtel */
+
+/* Visible outside config.c so that platforms can use it to recognise
+ * the proxy type control */
+void proxy_type_handler(dlgcontrol *ctrl, dlgparam *dlg,
+                        void *data, int event);
+/* And then they'll set this flag in its generic.context.i */
+#define PROXY_UI_FLAG_LOCAL 1 /* has a local proxy */
+
+/*
+ * Exports from bidi.c.
+ */
+#define BIDI_CHAR_INDEX_NONE ((unsigned short)-1)
+typedef struct bidi_char {
+    unsigned int origwc, wc;
+    unsigned short index, nchars;
+} bidi_char;
+BidiContext *bidi_new_context(void);
+void bidi_free_context(BidiContext *ctx);
+void do_bidi(BidiContext *ctx, bidi_char *line, size_t count);
+int do_shape(bidi_char *line, bidi_char *to, int count);
+bool is_rtl(int c);
+
+/*
+ * X11 auth mechanisms we know about.
+ */
+enum {
+    X11_NO_AUTH,
+    X11_MIT,                           /* MIT-MAGIC-COOKIE-1 */
+    X11_XDM,                           /* XDM-AUTHORIZATION-1 */
+    X11_NAUTHS
+};
+extern const char *const x11_authnames[X11_NAUTHS];
+
+/*
+ * An enum for the copy-paste UI action configuration.
+ */
+enum {
+    CLIPUI_NONE,     /* UI action has no copy/paste effect */
+    CLIPUI_IMPLICIT, /* use the default clipboard implicit in mouse actions  */
+    CLIPUI_EXPLICIT, /* use the default clipboard for explicit Copy/Paste */
+    CLIPUI_CUSTOM,   /* use a named clipboard (on systems that support it) */
+};
+
+/*
+ * Miscellaneous exports from the platform-specific code.
+ *
+ * filename_serialise and filename_deserialise have the same semantics
+ * as fontspec_serialise and fontspec_deserialise above.
+ */
+Filename *filename_from_str(const char *string);
+const char *filename_to_str(const Filename *fn);
+bool filename_equal(const Filename *f1, const Filename *f2);
+bool filename_is_null(const Filename *fn);
+Filename *filename_copy(const Filename *fn);
+void filename_free(Filename *fn);
+void filename_serialise(BinarySink *bs, const Filename *f);
+Filename *filename_deserialise(BinarySource *src);
+char *get_username(void);              /* return value needs freeing */
+char *get_random_data(int bytes, const char *device); /* used in cmdgen.c */
+char filename_char_sanitise(char c);   /* rewrite special pathname chars */
+bool open_for_write_would_lose_data(const Filename *fn);
+
+/*
+ * Exports and imports from timing.c.
+ *
+ * schedule_timer() asks the front end to schedule a callback to a
+ * timer function in a given number of ticks. The returned value is
+ * the time (in ticks since an arbitrary offset) at which the
+ * callback can be expected. This value will also be passed as the
+ * `now' parameter to the callback function. Hence, you can (for
+ * example) schedule an event at a particular time by calling
+ * schedule_timer() and storing the return value in your context
+ * structure as the time when that event is due. The first time a
+ * callback function gives you that value or more as `now', you do
+ * the thing.
+ *
+ * expire_timer_context() drops all current timers associated with
+ * a given value of ctx (for when you're about to free ctx).
+ *
+ * run_timers() is called from the front end when it has reason to
+ * think some timers have reached their moment, or when it simply
+ * needs to know how long to wait next. We pass it the time we
+ * think it is. It returns true and places the time when the next
+ * timer needs to go off in `next', or alternatively it returns
+ * false if there are no timers at all pending.
+ *
+ * timer_change_notify() must be supplied by the front end; it
+ * notifies the front end that a new timer has been added to the
+ * list which is sooner than any existing ones. It provides the
+ * time when that timer needs to go off.
+ *
+ * *** FRONT END IMPLEMENTORS NOTE:
+ *
+ * There's an important subtlety in the front-end implementation of
+ * the timer interface. When a front end is given a `next' value,
+ * either returned from run_timers() or via timer_change_notify(),
+ * it should ensure that it really passes _that value_ as the `now'
+ * parameter to its next run_timers call. It should _not_ simply
+ * call GETTICKCOUNT() to get the `now' parameter when invoking
+ * run_timers().
+ *
+ * The reason for this is that an OS's system clock might not agree
+ * exactly with the timing mechanisms it supplies to wait for a
+ * given interval. I'll illustrate this by the simple example of
+ * Unix Plink, which uses timeouts to poll() in a way which for
+ * these purposes can simply be considered to be a wait() function.
+ * Suppose, for the sake of argument, that this wait() function
+ * tends to return early by 1%. Then a possible sequence of actions
+ * is:
+ *
+ *  - run_timers() tells the front end that the next timer firing
+ *    is 10000ms from now.
+ *  - Front end calls wait(10000ms), but according to
+ *    GETTICKCOUNT() it has only waited for 9900ms.
+ *  - Front end calls run_timers() again, passing time T-100ms as
+ *    `now'.
+ *  - run_timers() does nothing, and says the next timer firing is
+ *    still 100ms from now.
+ *  - Front end calls wait(100ms), which only waits for 99ms.
+ *  - Front end calls run_timers() yet again, passing time T-1ms.
+ *  - run_timers() says there's still 1ms to wait.
+ *  - Front end calls wait(1ms).
+ *
+ * If you're _lucky_ at this point, wait(1ms) will actually wait
+ * for 1ms and you'll only have woken the program up three times.
+ * If you're unlucky, wait(1ms) might do nothing at all due to
+ * being below some minimum threshold, and you might find your
+ * program spends the whole of the last millisecond tight-looping
+ * between wait() and run_timers().
+ *
+ * Instead, what you should do is to _save_ the precise `next'
+ * value provided by run_timers() or via timer_change_notify(), and
+ * use that precise value as the input to the next run_timers()
+ * call. So:
+ *
+ *  - run_timers() tells the front end that the next timer firing
+ *    is at time T, 10000ms from now.
+ *  - Front end calls wait(10000ms).
+ *  - Front end then immediately calls run_timers() and passes it
+ *    time T, without stopping to check GETTICKCOUNT() at all.
+ *
+ * This guarantees that the program wakes up only as many times as
+ * there are actual timer actions to be taken, and that the timing
+ * mechanism will never send it into a tight loop.
+ *
+ * (It does also mean that the timer action in the above example
+ * will occur 100ms early, but this is not generally critical. And
+ * the hypothetical 1% error in wait() will be partially corrected
+ * for anyway when, _after_ run_timers() returns, you call
+ * GETTICKCOUNT() and compare the result with the returned `next'
+ * value to find out how long you have to make your next wait().)
+ */
+typedef void (*timer_fn_t)(void *ctx, unsigned long now);
+unsigned long schedule_timer(int ticks, timer_fn_t fn, void *ctx);
+void expire_timer_context(void *ctx);
+bool run_timers(unsigned long now, unsigned long *next);
+void timer_change_notify(unsigned long next);
+unsigned long timing_last_clock(void);
+
+/*
+ * Exports from callback.c.
+ *
+ * This provides a method of queuing function calls to be run at the
+ * earliest convenience from the top-level event loop. Use it if
+ * you're deep in a nested chain of calls and want to trigger an
+ * action which will probably lead to your function being re-entered
+ * recursively if you just call the initiating function the normal
+ * way.
+ *
+ * Most front ends run the queued callbacks by simply calling
+ * run_toplevel_callbacks() after handling each event in their
+ * top-level event loop. However, if a front end doesn't have control
+ * over its own event loop (e.g. because it's using GTK) then it can
+ * instead request notifications when a callback is available, so that
+ * it knows to ask its delegate event loop to do the same thing. Also,
+ * if a front end needs to know whether a callback is pending without
+ * actually running it (e.g. so as to put a zero timeout on a poll()
+ * call) then it can call toplevel_callback_pending(), which will
+ * return true if at least one callback is in the queue.
+ *
+ * run_toplevel_callbacks() returns true if it ran any actual code.
+ * This can be used as a means of speculatively terminating a poll
+ * loop, as in PSFTP, for example - if a callback has run then perhaps
+ * it might have done whatever the loop's caller was waiting for.
+ */
+void queue_toplevel_callback(toplevel_callback_fn_t fn, void *ctx);
+bool run_toplevel_callbacks(void);
+bool toplevel_callback_pending(void);
+void delete_callbacks_for_context(void *ctx);
+
+/*
+ * Another facility in callback.c deals with 'idempotent' callbacks,
+ * defined as those which never need to be scheduled again if they are
+ * already scheduled and have not yet run. (An example would be one
+ * which, when called, empties a queue of data completely: when data
+ * is added to the queue, you must ensure a run of the queue-consuming
+ * function has been scheduled, but if one is already pending, you
+ * don't need to schedule a second one.)
+ */
+struct IdempotentCallback {
+    toplevel_callback_fn_t fn;
+    void *ctx;
+    bool queued;
+};
+void queue_idempotent_callback(struct IdempotentCallback *ic);
+
+typedef void (*toplevel_callback_notify_fn_t)(void *ctx);
+void request_callback_notifications(toplevel_callback_notify_fn_t notify,
+                                    void *ctx);
+
+/*
+ * Facility provided by the platform to spawn a parallel subprocess
+ * and present its stdio via a Socket.
+ *
+ * 'prefix' indicates the prefix that should appear on messages passed
+ * to plug_log to provide stderr output from the process.
+ */
+Socket *platform_start_subprocess(const char *cmd, Plug *plug,
+                                  const char *prefix);
+
+/*
+ * Define no-op macros for the jump list functions, on platforms that
+ * don't support them. (This is a bit of a hack, and it'd be nicer to
+ * localise even the calls to those functions into the Windows front
+ * end, but it'll do for the moment.)
+ */
+#ifndef JUMPLIST_SUPPORTED
+#define add_session_to_jumplist(x) ((void)0)
+#define remove_session_from_jumplist(x) ((void)0)
+#endif
+
+/* SURROGATE PAIR */
+#ifndef HIGH_SURROGATE_START /* in some toolchains <winnls.h> defines these */
+#define HIGH_SURROGATE_START 0xd800
+#define HIGH_SURROGATE_END 0xdbff
+#define LOW_SURROGATE_START 0xdc00
+#define LOW_SURROGATE_END 0xdfff
+#endif
+
+/* These macros exist in the Windows API, so the environment may
+ * provide them. If not, define them in terms of the above. */
+#ifndef IS_HIGH_SURROGATE
+#define IS_HIGH_SURROGATE(wch) (((wch) >= HIGH_SURROGATE_START) && \
+                                ((wch) <= HIGH_SURROGATE_END))
+#define IS_LOW_SURROGATE(wch) (((wch) >= LOW_SURROGATE_START) && \
+                               ((wch) <= LOW_SURROGATE_END))
+#define IS_SURROGATE_PAIR(hs, ls) (IS_HIGH_SURROGATE(hs) && \
+                                   IS_LOW_SURROGATE(ls))
+#endif
+
+
+#define IS_SURROGATE(wch) (((wch) >= HIGH_SURROGATE_START) &&   \
+                           ((wch) <= LOW_SURROGATE_END))
+#define HIGH_SURROGATE_OF(codept) \
+    (HIGH_SURROGATE_START + (((codept) - 0x10000) >> 10))
+#define LOW_SURROGATE_OF(codept) \
+    (LOW_SURROGATE_START + (((codept) - 0x10000) & 0x3FF))
+#define FROM_SURROGATES(wch1, wch2) \
+    (0x10000 + (((wch1) & 0x3FF) << 10) + ((wch2) & 0x3FF))
+
+#endif
diff --git putty-0.81/settings.c putty-url-0.81/settings.c
index 4f56937..119f5af 100644
--- putty-0.81/settings.c
+++ putty-url-0.81/settings.c
@@ -11,6 +11,8 @@
 #include "ssh/gssc.h"
 #include "ssh/gss.h"
 #endif
+#include "urlhack.h"
+#include <versionhelpers.h>
 
 
 /* The cipher order given here is the default order. */
@@ -815,6 +817,14 @@ void save_open_settings(settings_w *sesskey, Conf *conf)
     write_setting_i(sesskey, "SUPDUPCharset", conf_get_int(conf, CONF_supdup_ascii_set));
     write_setting_b(sesskey, "SUPDUPMoreProcessing", conf_get_bool(conf, CONF_supdup_more));
     write_setting_b(sesskey, "SUPDUPScrolling", conf_get_bool(conf, CONF_supdup_scroll));
+
+    /* PuTTY-url */
+    write_setting_i(sesskey, "HyperlinkUnderline", conf_get_int(conf, CONF_url_underline));
+    write_setting_b(sesskey, "HyperlinkUseCtrlClick", conf_get_bool(conf, CONF_url_ctrl_click));
+    write_setting_b(sesskey, "HyperlinkBrowserUseDefault", conf_get_bool(conf, CONF_url_defbrowser));
+    write_setting_filename(sesskey, "HyperlinkBrowser", conf_get_filename(conf, CONF_url_browser));
+    write_setting_b(sesskey, "HyperlinkRegularExpressionUseDefault", conf_get_bool(conf, CONF_url_defregex));
+    write_setting_s(sesskey, "HyperlinkRegularExpression", conf_get_str(conf, CONF_url_regex));
 }
 
 bool load_settings(const char *section, Conf *conf)
@@ -1139,7 +1149,7 @@ void load_open_settings(settings_r *sesskey, Conf *conf)
                  / 1000
 #endif
                  );
-    gppi(sesskey, "ScrollbackLines", 2000, conf, CONF_savelines);
+    gppi(sesskey, "ScrollbackLines", 100000, conf, CONF_savelines); /* HACK: PuTTY-url */
     gppb(sesskey, "DECOriginMode", false, conf, CONF_dec_om);
     gppb(sesskey, "AutoWrapMode", true, conf, CONF_wrap_mode);
     gppb(sesskey, "LFImpliesCR", false, conf, CONF_lfhascr);
@@ -1151,7 +1161,14 @@ void load_open_settings(settings_r *sesskey, Conf *conf)
     gppi(sesskey, "TermWidth", 80, conf, CONF_width);
     gppi(sesskey, "TermHeight", 24, conf, CONF_height);
     gppfont(sesskey, "Font", conf, CONF_font);
-    gppi(sesskey, "FontQuality", FQ_DEFAULT, conf, CONF_font_quality);
+
+    /* HACK: PuTTY-url: Set font quality to cleartype on Windows Vista and higher */
+    if (IsWindowsVistaOrGreater()) {
+        gppi(sesskey, "FontQuality", FQ_CLEARTYPE, conf, CONF_font_quality);
+    } else {
+        gppi(sesskey, "FontQuality", FQ_DEFAULT, conf, CONF_font_quality);
+    }
+
     gppi(sesskey, "FontVTMode", VT_UNICODE, conf, CONF_vtmode);
     gppb(sesskey, "UseSystemColours", false, conf, CONF_system_colour);
     gppb(sesskey, "TryPalette", false, conf, CONF_try_palette);
@@ -1301,6 +1318,14 @@ void load_open_settings(settings_r *sesskey, Conf *conf)
     gppi(sesskey, "SUPDUPCharset", false, conf, CONF_supdup_ascii_set);
     gppb(sesskey, "SUPDUPMoreProcessing", false, conf, CONF_supdup_more);
     gppb(sesskey, "SUPDUPScrolling", false, conf, CONF_supdup_scroll);
+
+    /* PuTTY-url */
+    gppi(sesskey, "HyperlinkUnderline", 1, conf, CONF_url_underline);
+    gppb(sesskey, "HyperlinkUseCtrlClick", false, conf, CONF_url_ctrl_click);
+    gppb(sesskey, "HyperlinkBrowserUseDefault", true, conf, CONF_url_defbrowser);
+    gppfile(sesskey, "HyperlinkBrowser", conf, CONF_url_browser);
+    gppb(sesskey, "HyperlinkRegularExpressionUseDefault", true, conf, CONF_url_defregex);
+    gpps(sesskey, "HyperlinkRegularExpression", URLHACK_DEFAULT_REGEX, conf, CONF_url_regex);
 }
 
 bool do_defaults(const char *session, Conf *conf)
diff --git putty-url-0.81/settings.c.orig putty-url-0.81/settings.c.orig
new file mode 100644
index 0000000..bd7ca60
--- /dev/null
+++ putty-url-0.81/settings.c.orig
@@ -0,0 +1,1391 @@
+/*
+ * settings.c: read and write saved sessions. (platform-independent)
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include "putty.h"
+#include "storage.h"
+#ifndef NO_GSSAPI
+#include "ssh/gssc.h"
+#include "ssh/gss.h"
+#endif
+
+
+/* The cipher order given here is the default order. */
+static const struct keyvalwhere ciphernames[] = {
+    { "aes",        CIPHER_AES,             -1, -1 },
+    { "chacha20",   CIPHER_CHACHA20,        CIPHER_AES, +1 },
+    { "aesgcm",     CIPHER_AESGCM,          CIPHER_CHACHA20, +1 },
+    { "3des",       CIPHER_3DES,            -1, -1 },
+    { "WARN",       CIPHER_WARN,            -1, -1 },
+    { "des",        CIPHER_DES,             -1, -1 },
+    { "blowfish",   CIPHER_BLOWFISH,        -1, -1 },
+    { "arcfour",    CIPHER_ARCFOUR,         -1, -1 },
+};
+
+/* The default order here is sometimes overridden by the backward-
+ * compatibility warts in load_open_settings(), and should be kept
+ * in sync with those. */
+static const struct keyvalwhere kexnames[] = {
+    { "ntru-curve25519",    KEX_NTRU_HYBRID, -1, +1 },
+    { "ecdh",               KEX_ECDH,       -1, +1 },
+    /* This name is misleading: it covers both SHA-256 and SHA-1 variants */
+    { "dh-gex-sha1",        KEX_DHGEX,      -1, -1 },
+    /* Again, this covers both SHA-256 and SHA-1, despite the name: */
+    { "dh-group14-sha1",    KEX_DHGROUP14,  -1, -1 },
+    /* This one really is only SHA-1, though: */
+    { "dh-group1-sha1",     KEX_DHGROUP1,   KEX_WARN, +1 },
+    { "rsa",                KEX_RSA,        KEX_WARN, -1 },
+    /* Larger fixed DH groups: prefer the larger 15 and 16 over 14,
+     * but by default the even larger 17 and 18 go below 16.
+     * Rationale: diminishing returns of improving the DH strength are
+     * outweighed by increased CPU cost. Group 18 is painful on a slow
+     * machine. Users can override if they need to. */
+    { "dh-group15-sha512",  KEX_DHGROUP15,  KEX_DHGROUP14, -1 },
+    { "dh-group16-sha512",  KEX_DHGROUP16,  KEX_DHGROUP15, -1 },
+    { "dh-group17-sha512",  KEX_DHGROUP17,  KEX_DHGROUP16, +1 },
+    { "dh-group18-sha512",  KEX_DHGROUP18,  KEX_DHGROUP17, +1 },
+    { "WARN",               KEX_WARN,       -1, -1 }
+};
+
+static const struct keyvalwhere hknames[] = {
+    { "ed25519",    HK_ED25519,             -1, +1 },
+    { "ed448",      HK_ED448,               -1, +1 },
+    { "ecdsa",      HK_ECDSA,               -1, -1 },
+    { "dsa",        HK_DSA,                 -1, -1 },
+    { "rsa",        HK_RSA,                 -1, -1 },
+    { "WARN",       HK_WARN,                -1, -1 },
+};
+
+/*
+ * All the terminal modes that we know about for the "TerminalModes"
+ * setting. (Also used by config.c for the drop-down list.)
+ * This is currently precisely the same as the set in
+ * ssh/ttymode-list.h, but could in principle differ if other backends
+ * started to support tty modes (e.g., the pty backend).
+ * The set of modes in in this array is currently significant for
+ * settings migration from old versions; if they change, review the
+ * gppmap() invocation for "TerminalModes".
+ */
+const char *const ttymodes[] = {
+    "INTR",     "QUIT",     "ERASE",    "KILL",     "EOF",
+    "EOL",      "EOL2",     "START",    "STOP",     "SUSP",
+    "DSUSP",    "REPRINT",  "WERASE",   "LNEXT",    "FLUSH",
+    "SWTCH",    "STATUS",   "DISCARD",  "IGNPAR",   "PARMRK",
+    "INPCK",    "ISTRIP",   "INLCR",    "IGNCR",    "ICRNL",
+    "IUCLC",    "IXON",     "IXANY",    "IXOFF",    "IMAXBEL",
+    "IUTF8",    "ISIG",     "ICANON",   "XCASE",    "ECHO",
+    "ECHOE",    "ECHOK",    "ECHONL",   "NOFLSH",   "TOSTOP",
+    "IEXTEN",   "ECHOCTL",  "ECHOKE",   "PENDIN",   "OPOST",
+    "OLCUC",    "ONLCR",    "OCRNL",    "ONOCR",    "ONLRET",
+    "CS7",      "CS8",      "PARENB",   "PARODD",   NULL
+};
+
+static int default_protocol, default_port;
+void settings_set_default_protocol(int newval) { default_protocol = newval; }
+void settings_set_default_port(int newval) { default_port = newval; }
+
+/*
+ * Convenience functions to access the backends[] array
+ * (which is only present in tools that manage settings).
+ */
+
+const struct BackendVtable *backend_vt_from_name(const char *name)
+{
+    const struct BackendVtable *const *p;
+    for (p = backends; *p != NULL; p++)
+        if (!strcmp((*p)->id, name))
+            return *p;
+    return NULL;
+}
+
+const struct BackendVtable *backend_vt_from_proto(int proto)
+{
+    const struct BackendVtable *const *p;
+    for (p = backends; *p != NULL; p++)
+        if ((*p)->protocol == proto)
+            return *p;
+    return NULL;
+}
+
+char *get_remote_username(Conf *conf)
+{
+    char *username = conf_get_str(conf, CONF_username);
+    if (*username) {
+        return dupstr(username);
+    } else if (conf_get_bool(conf, CONF_username_from_env)) {
+        /* Use local username. */
+        return get_username();     /* might still be NULL */
+    } else {
+        return NULL;
+    }
+}
+
+static char *gpps_raw(settings_r *sesskey, const char *name, const char *def)
+{
+    char *ret = read_setting_s(sesskey, name);
+    if (!ret)
+        ret = platform_default_s(name);
+    if (!ret)
+        ret = def ? dupstr(def) : NULL;   /* permit NULL as final fallback */
+    return ret;
+}
+
+static void gpps(settings_r *sesskey, const char *name, const char *def,
+                 Conf *conf, int primary)
+{
+    char *val = gpps_raw(sesskey, name, def);
+    conf_set_str(conf, primary, val);
+    sfree(val);
+}
+
+/*
+ * gppfont and gppfile cannot have local defaults, since the very
+ * format of a Filename or FontSpec is platform-dependent. So the
+ * platform-dependent functions MUST return some sort of value.
+ */
+static void gppfont(settings_r *sesskey, char *name,
+                    Conf *conf, int primary)
+{
+    FontSpec *result = read_setting_fontspec(sesskey, name);
+    if (!result)
+        result = platform_default_fontspec(name);
+    conf_set_fontspec(conf, primary, result);
+    fontspec_free(result);
+}
+static void gppfile(settings_r *sesskey, const char *name,
+                    Conf *conf, int primary)
+{
+    Filename *result = read_setting_filename(sesskey, name);
+    if (!result)
+        result = platform_default_filename(name);
+    conf_set_filename(conf, primary, result);
+    filename_free(result);
+}
+
+static bool gppb_raw(settings_r *sesskey, const char *name, bool def)
+{
+    def = platform_default_b(name, def);
+    return sesskey ? read_setting_i(sesskey, name, def) != 0 : def;
+}
+
+static void gppb(settings_r *sesskey, const char *name, bool def,
+                 Conf *conf, int primary)
+{
+    conf_set_bool(conf, primary, gppb_raw(sesskey, name, def));
+}
+
+static int gppi_raw(settings_r *sesskey, const char *name, int def)
+{
+    def = platform_default_i(name, def);
+    return read_setting_i(sesskey, name, def);
+}
+
+static void gppi(settings_r *sesskey, const char *name, int def,
+                 Conf *conf, int primary)
+{
+    conf_set_int(conf, primary, gppi_raw(sesskey, name, def));
+}
+
+/*
+ * Read a set of name-value pairs in the format we occasionally use:
+ *   NAME\tVALUE\0NAME\tVALUE\0\0 in memory
+ *   NAME=VALUE,NAME=VALUE, in storage
+ * If there's no "=VALUE" (e.g. just NAME,NAME,NAME) then those keys
+ * are mapped to the empty string.
+ */
+static bool gppmap(settings_r *sesskey, const char *name,
+                   Conf *conf, int primary)
+{
+    char *buf, *p, *q, *key, *val;
+
+    /*
+     * Start by clearing any existing subkeys of this key from conf.
+     */
+    while ((key = conf_get_str_nthstrkey(conf, primary, 0)) != NULL)
+        conf_del_str_str(conf, primary, key);
+
+    /*
+     * Now read a serialised list from the settings and unmarshal it
+     * into its components.
+     */
+    buf = gpps_raw(sesskey, name, NULL);
+    if (!buf)
+        return false;
+
+    p = buf;
+    while (*p) {
+        q = buf;
+        val = NULL;
+        while (*p && *p != ',') {
+            int c = *p++;
+            if (c == '=')
+                c = '\0';
+            if (c == '\\')
+                c = *p++;
+            *q++ = c;
+            if (!c)
+                val = q;
+        }
+        if (*p == ',')
+            p++;
+        if (!val)
+            val = q;
+        *q = '\0';
+
+        if (primary == CONF_portfwd && strchr(buf, 'D') != NULL) {
+            /*
+             * Backwards-compatibility hack: dynamic forwardings are
+             * indexed in the data store as a third type letter in the
+             * key, 'D' alongside 'L' and 'R' - but really, they
+             * should be filed under 'L' with a special _value_,
+             * because local and dynamic forwardings both involve
+             * _listening_ on a local port, and are hence mutually
+             * exclusive on the same port number. So here we translate
+             * the legacy storage format into the sensible internal
+             * form, by finding the D and turning it into a L.
+             */
+            char *newkey = dupstr(buf);
+            *strchr(newkey, 'D') = 'L';
+            conf_set_str_str(conf, primary, newkey, "D");
+            sfree(newkey);
+        } else {
+            conf_set_str_str(conf, primary, buf, val);
+        }
+    }
+    sfree(buf);
+
+    return true;
+}
+
+/*
+ * Write a set of name/value pairs in the above format, or just the
+ * names if include_values is false.
+ */
+static void wmap(settings_w *sesskey, char const *outkey, Conf *conf,
+                 int primary, bool include_values)
+{
+    char *buf, *p, *key, *realkey;
+    const char *val, *q;
+    int len;
+
+    len = 1;                           /* allow for NUL */
+
+    for (val = conf_get_str_strs(conf, primary, NULL, &key);
+         val != NULL;
+         val = conf_get_str_strs(conf, primary, key, &key))
+        len += 2 + 2 * (strlen(key) + strlen(val));   /* allow for escaping */
+
+    buf = snewn(len, char);
+    p = buf;
+
+    for (val = conf_get_str_strs(conf, primary, NULL, &key);
+         val != NULL;
+         val = conf_get_str_strs(conf, primary, key, &key)) {
+
+        if (primary == CONF_portfwd && !strcmp(val, "D")) {
+            /*
+             * Backwards-compatibility hack, as above: translate from
+             * the sensible internal representation of dynamic
+             * forwardings (key "L<port>", value "D") to the
+             * conceptually incoherent legacy storage format (key
+             * "D<port>", value empty).
+             */
+            char *L;
+
+            realkey = key;             /* restore it at end of loop */
+            val = "";
+            key = dupstr(key);
+            L = strchr(key, 'L');
+            if (L) *L = 'D';
+        } else {
+            realkey = NULL;
+        }
+
+        if (p != buf)
+            *p++ = ',';
+        for (q = key; *q; q++) {
+            if (*q == '=' || *q == ',' || *q == '\\')
+                *p++ = '\\';
+            *p++ = *q;
+        }
+        if (include_values) {
+            *p++ = '=';
+            for (q = val; *q; q++) {
+                if (*q == '=' || *q == ',' || *q == '\\')
+                    *p++ = '\\';
+                *p++ = *q;
+            }
+        }
+
+        if (realkey) {
+            free(key);
+            key = realkey;
+        }
+    }
+    *p = '\0';
+    write_setting_s(sesskey, outkey, buf);
+    sfree(buf);
+}
+
+static int key2val(const struct keyvalwhere *mapping,
+                   int nmaps, char *key)
+{
+    int i;
+    for (i = 0; i < nmaps; i++)
+        if (!strcmp(mapping[i].s, key)) return mapping[i].v;
+    return -1;
+}
+
+static const char *val2key(const struct keyvalwhere *mapping,
+                           int nmaps, int val)
+{
+    int i;
+    for (i = 0; i < nmaps; i++)
+        if (mapping[i].v == val) return mapping[i].s;
+    return NULL;
+}
+
+/*
+ * Helper function to parse a comma-separated list of strings into
+ * a preference list array of values. Any missing values are added
+ * to the end and duplicates are weeded.
+ * XXX: assumes vals in 'mapping' are small +ve integers
+ */
+static void gprefs_from_str(const char *str,
+                            const struct keyvalwhere *mapping, int nvals,
+                            Conf *conf, int primary)
+{
+    char *commalist = dupstr(str);
+    char *p, *q;
+    int i, j, n, v, pos;
+    unsigned long seen = 0;            /* bitmap for weeding dups etc */
+
+    /*
+     * Go through that list and convert it into values.
+     */
+    n = 0;
+    p = commalist;
+    while (1) {
+        while (*p && *p == ',') p++;
+        if (!*p)
+            break;                     /* no more words */
+
+        q = p;
+        while (*p && *p != ',') p++;
+        if (*p) *p++ = '\0';
+
+        v = key2val(mapping, nvals, q);
+        if (v != -1 && !(seen & (1 << v))) {
+            seen |= (1 << v);
+            conf_set_int_int(conf, primary, n, v);
+            n++;
+        }
+    }
+
+    sfree(commalist);
+
+    /*
+     * Now go through 'mapping' and add values that weren't mentioned
+     * in the list we fetched. We may have to loop over it multiple
+     * times so that we add values before other values whose default
+     * positions depend on them.
+     */
+    while (n < nvals) {
+        for (i = 0; i < nvals; i++) {
+            assert(mapping[i].v >= 0);
+            assert(mapping[i].v < 32);
+
+            if (!(seen & (1 << mapping[i].v))) {
+                /*
+                 * This element needs adding. But can we add it yet?
+                 */
+                if (mapping[i].vrel != -1 && !(seen & (1 << mapping[i].vrel)))
+                    continue;          /* nope */
+
+                /*
+                 * OK, we can work out where to add this element, so
+                 * do so.
+                 */
+                if (mapping[i].vrel == -1) {
+                    pos = (mapping[i].where < 0 ? n : 0);
+                } else {
+                    for (j = 0; j < n; j++)
+                        if (conf_get_int_int(conf, primary, j) ==
+                            mapping[i].vrel)
+                            break;
+                    assert(j < n);     /* implied by (seen & (1<<vrel)) */
+                    pos = (mapping[i].where < 0 ? j : j+1);
+                }
+
+                /*
+                 * And add it.
+                 */
+                for (j = n-1; j >= pos; j--)
+                    conf_set_int_int(conf, primary, j+1,
+                                     conf_get_int_int(conf, primary, j));
+                conf_set_int_int(conf, primary, pos, mapping[i].v);
+                seen |= (1 << mapping[i].v);
+                n++;
+            }
+        }
+    }
+}
+
+/*
+ * Read a preference list.
+ */
+static void gprefs(settings_r *sesskey, const char *name, const char *def,
+                   const struct keyvalwhere *mapping, int nvals,
+                   Conf *conf, int primary)
+{
+    /*
+     * Fetch the string which we'll parse as a comma-separated list.
+     */
+    char *value = gpps_raw(sesskey, name, def);
+    gprefs_from_str(value, mapping, nvals, conf, primary);
+    sfree(value);
+}
+
+/*
+ * Write out a preference list.
+ */
+static void wprefs(settings_w *sesskey, const char *name,
+                   const struct keyvalwhere *mapping, int nvals,
+                   Conf *conf, int primary)
+{
+    char *buf, *p;
+    int i, maxlen;
+
+    for (maxlen = i = 0; i < nvals; i++) {
+        const char *s = val2key(mapping, nvals,
+                                conf_get_int_int(conf, primary, i));
+        if (s) {
+            maxlen += (maxlen > 0 ? 1 : 0) + strlen(s);
+        }
+    }
+
+    buf = snewn(maxlen + 1, char);
+    p = buf;
+
+    for (i = 0; i < nvals; i++) {
+        const char *s = val2key(mapping, nvals,
+                                conf_get_int_int(conf, primary, i));
+        if (s) {
+            p += sprintf(p, "%s%s", (p > buf ? "," : ""), s);
+        }
+    }
+
+    assert(p - buf == maxlen);
+    *p = '\0';
+
+    write_setting_s(sesskey, name, buf);
+
+    sfree(buf);
+}
+
+static void write_setting_b(settings_w *handle, const char *key, bool value)
+{
+    write_setting_i(handle, key, value ? 1 : 0);
+}
+
+static void write_clip_setting(settings_w *sesskey, const char *savekey,
+                               Conf *conf, int confkey, int strconfkey)
+{
+    int val = conf_get_int(conf, confkey);
+    switch (val) {
+      case CLIPUI_NONE:
+      default:
+        write_setting_s(sesskey, savekey, "none");
+        break;
+      case CLIPUI_IMPLICIT:
+        write_setting_s(sesskey, savekey, "implicit");
+        break;
+      case CLIPUI_EXPLICIT:
+        write_setting_s(sesskey, savekey, "explicit");
+        break;
+      case CLIPUI_CUSTOM: {
+        char *sval = dupcat("custom:", conf_get_str(conf, strconfkey));
+        write_setting_s(sesskey, savekey, sval);
+        sfree(sval);
+        break;
+      }
+    }
+}
+
+static void read_clip_setting(settings_r *sesskey, char *savekey,
+                              int def, Conf *conf, int confkey, int strconfkey)
+{
+    char *setting = read_setting_s(sesskey, savekey);
+    int val;
+
+    conf_set_str(conf, strconfkey, "");
+    if (!setting) {
+        val = def;
+    } else if (!strcmp(setting, "implicit")) {
+        val = CLIPUI_IMPLICIT;
+    } else if (!strcmp(setting, "explicit")) {
+        val = CLIPUI_EXPLICIT;
+    } else if (!strncmp(setting, "custom:", 7)) {
+        val = CLIPUI_CUSTOM;
+        conf_set_str(conf, strconfkey, setting + 7);
+    } else {
+        val = CLIPUI_NONE;
+    }
+    conf_set_int(conf, confkey, val);
+    sfree(setting);
+}
+
+char *save_settings(const char *section, Conf *conf)
+{
+    struct settings_w *sesskey;
+    char *errmsg;
+
+    sesskey = open_settings_w(section, &errmsg);
+    if (!sesskey)
+        return errmsg;
+    save_open_settings(sesskey, conf);
+    close_settings_w(sesskey);
+    return NULL;
+}
+
+void save_open_settings(settings_w *sesskey, Conf *conf)
+{
+    int i;
+    const char *p;
+
+    write_setting_i(sesskey, "Present", 1);
+    write_setting_s(sesskey, "HostName", conf_get_str(conf, CONF_host));
+    write_setting_filename(sesskey, "LogFileName", conf_get_filename(conf, CONF_logfilename));
+    write_setting_i(sesskey, "LogType", conf_get_int(conf, CONF_logtype));
+    write_setting_i(sesskey, "LogFileClash", conf_get_int(conf, CONF_logxfovr));
+    write_setting_b(sesskey, "LogFlush", conf_get_bool(conf, CONF_logflush));
+    write_setting_b(sesskey, "LogHeader", conf_get_bool(conf, CONF_logheader));
+    write_setting_b(sesskey, "SSHLogOmitPasswords", conf_get_bool(conf, CONF_logomitpass));
+    write_setting_b(sesskey, "SSHLogOmitData", conf_get_bool(conf, CONF_logomitdata));
+    p = "raw";
+    {
+        const struct BackendVtable *vt =
+            backend_vt_from_proto(conf_get_int(conf, CONF_protocol));
+        if (vt)
+            p = vt->id;
+    }
+    write_setting_s(sesskey, "Protocol", p);
+    write_setting_i(sesskey, "PortNumber", conf_get_int(conf, CONF_port));
+    /* The CloseOnExit numbers are arranged in a different order from
+     * the standard FORCE_ON / FORCE_OFF / AUTO. */
+    write_setting_i(sesskey, "CloseOnExit", (conf_get_int(conf, CONF_close_on_exit)+2)%3);
+    write_setting_b(sesskey, "WarnOnClose", !!conf_get_bool(conf, CONF_warn_on_close));
+    write_setting_i(sesskey, "PingInterval", conf_get_int(conf, CONF_ping_interval) / 60);      /* minutes */
+    write_setting_i(sesskey, "PingIntervalSecs", conf_get_int(conf, CONF_ping_interval) % 60);  /* seconds */
+    write_setting_b(sesskey, "TCPNoDelay", conf_get_bool(conf, CONF_tcp_nodelay));
+    write_setting_b(sesskey, "TCPKeepalives", conf_get_bool(conf, CONF_tcp_keepalives));
+    write_setting_s(sesskey, "TerminalType", conf_get_str(conf, CONF_termtype));
+    write_setting_s(sesskey, "TerminalSpeed", conf_get_str(conf, CONF_termspeed));
+    wmap(sesskey, "TerminalModes", conf, CONF_ttymodes, true);
+
+    /* Address family selection */
+    write_setting_i(sesskey, "AddressFamily", conf_get_int(conf, CONF_addressfamily));
+
+    /* proxy settings */
+    write_setting_s(sesskey, "ProxyExcludeList", conf_get_str(conf, CONF_proxy_exclude_list));
+    write_setting_i(sesskey, "ProxyDNS", (conf_get_int(conf, CONF_proxy_dns)+2)%3);
+    write_setting_b(sesskey, "ProxyLocalhost", conf_get_bool(conf, CONF_even_proxy_localhost));
+    write_setting_i(sesskey, "ProxyMethod", conf_get_int(conf, CONF_proxy_type));
+    write_setting_s(sesskey, "ProxyHost", conf_get_str(conf, CONF_proxy_host));
+    write_setting_i(sesskey, "ProxyPort", conf_get_int(conf, CONF_proxy_port));
+    write_setting_s(sesskey, "ProxyUsername", conf_get_str(conf, CONF_proxy_username));
+    write_setting_s(sesskey, "ProxyPassword", conf_get_str(conf, CONF_proxy_password));
+    write_setting_s(sesskey, "ProxyTelnetCommand", conf_get_str(conf, CONF_proxy_telnet_command));
+    write_setting_i(sesskey, "ProxyLogToTerm", conf_get_int(conf, CONF_proxy_log_to_term));
+    wmap(sesskey, "Environment", conf, CONF_environmt, true);
+    write_setting_s(sesskey, "UserName", conf_get_str(conf, CONF_username));
+    write_setting_b(sesskey, "UserNameFromEnvironment", conf_get_bool(conf, CONF_username_from_env));
+    write_setting_s(sesskey, "LocalUserName", conf_get_str(conf, CONF_localusername));
+    write_setting_b(sesskey, "NoPTY", conf_get_bool(conf, CONF_nopty));
+    write_setting_b(sesskey, "Compression", conf_get_bool(conf, CONF_compression));
+    write_setting_b(sesskey, "TryAgent", conf_get_bool(conf, CONF_tryagent));
+    write_setting_b(sesskey, "AgentFwd", conf_get_bool(conf, CONF_agentfwd));
+#ifndef NO_GSSAPI
+    write_setting_b(sesskey, "GssapiFwd", conf_get_bool(conf, CONF_gssapifwd));
+#endif
+    write_setting_b(sesskey, "ChangeUsername", conf_get_bool(conf, CONF_change_username));
+    wprefs(sesskey, "Cipher", ciphernames, CIPHER_MAX, conf, CONF_ssh_cipherlist);
+    wprefs(sesskey, "KEX", kexnames, KEX_MAX, conf, CONF_ssh_kexlist);
+    wprefs(sesskey, "HostKey", hknames, HK_MAX, conf, CONF_ssh_hklist);
+    write_setting_b(sesskey, "PreferKnownHostKeys", conf_get_bool(conf, CONF_ssh_prefer_known_hostkeys));
+    write_setting_i(sesskey, "RekeyTime", conf_get_int(conf, CONF_ssh_rekey_time));
+#ifndef NO_GSSAPI
+    write_setting_i(sesskey, "GssapiRekey", conf_get_int(conf, CONF_gssapirekey));
+#endif
+    write_setting_s(sesskey, "RekeyBytes", conf_get_str(conf, CONF_ssh_rekey_data));
+    write_setting_b(sesskey, "SshNoAuth", conf_get_bool(conf, CONF_ssh_no_userauth));
+    write_setting_b(sesskey, "SshNoTrivialAuth", conf_get_bool(conf, CONF_ssh_no_trivial_userauth));
+    write_setting_b(sesskey, "SshBanner", conf_get_bool(conf, CONF_ssh_show_banner));
+    write_setting_b(sesskey, "AuthTIS", conf_get_bool(conf, CONF_try_tis_auth));
+    write_setting_b(sesskey, "AuthKI", conf_get_bool(conf, CONF_try_ki_auth));
+#ifndef NO_GSSAPI
+    write_setting_b(sesskey, "AuthGSSAPI", conf_get_bool(conf, CONF_try_gssapi_auth));
+    write_setting_b(sesskey, "AuthGSSAPIKEX", conf_get_bool(conf, CONF_try_gssapi_kex));
+    wprefs(sesskey, "GSSLibs", gsslibkeywords, ngsslibs, conf, CONF_ssh_gsslist);
+    write_setting_filename(sesskey, "GSSCustom", conf_get_filename(conf, CONF_ssh_gss_custom));
+#endif
+    write_setting_b(sesskey, "SshNoShell", conf_get_bool(conf, CONF_ssh_no_shell));
+    write_setting_i(sesskey, "SshProt", conf_get_int(conf, CONF_sshprot));
+    write_setting_s(sesskey, "LogHost", conf_get_str(conf, CONF_loghost));
+    write_setting_b(sesskey, "SSH2DES", conf_get_bool(conf, CONF_ssh2_des_cbc));
+    write_setting_filename(sesskey, "PublicKeyFile", conf_get_filename(conf, CONF_keyfile));
+    write_setting_filename(sesskey, "DetachedCertificate", conf_get_filename(conf, CONF_detached_cert));
+    write_setting_s(sesskey, "AuthPlugin", conf_get_str(conf, CONF_auth_plugin));
+    write_setting_s(sesskey, "RemoteCommand", conf_get_str(conf, CONF_remote_cmd));
+    write_setting_b(sesskey, "RFCEnviron", conf_get_bool(conf, CONF_rfc_environ));
+    write_setting_b(sesskey, "PassiveTelnet", conf_get_bool(conf, CONF_passive_telnet));
+    write_setting_b(sesskey, "BackspaceIsDelete", conf_get_bool(conf, CONF_bksp_is_delete));
+    write_setting_b(sesskey, "RXVTHomeEnd", conf_get_bool(conf, CONF_rxvt_homeend));
+    write_setting_i(sesskey, "LinuxFunctionKeys", conf_get_int(conf, CONF_funky_type));
+    write_setting_i(sesskey, "ShiftedArrowKeys", conf_get_int(conf, CONF_sharrow_type));
+    write_setting_b(sesskey, "NoApplicationKeys", conf_get_bool(conf, CONF_no_applic_k));
+    write_setting_b(sesskey, "NoApplicationCursors", conf_get_bool(conf, CONF_no_applic_c));
+    write_setting_b(sesskey, "NoMouseReporting", conf_get_bool(conf, CONF_no_mouse_rep));
+    write_setting_b(sesskey, "NoRemoteResize", conf_get_bool(conf, CONF_no_remote_resize));
+    write_setting_b(sesskey, "NoAltScreen", conf_get_bool(conf, CONF_no_alt_screen));
+    write_setting_b(sesskey, "NoRemoteWinTitle", conf_get_bool(conf, CONF_no_remote_wintitle));
+    write_setting_b(sesskey, "NoRemoteClearScroll", conf_get_bool(conf, CONF_no_remote_clearscroll));
+    write_setting_i(sesskey, "RemoteQTitleAction", conf_get_int(conf, CONF_remote_qtitle_action));
+    write_setting_b(sesskey, "NoDBackspace", conf_get_bool(conf, CONF_no_dbackspace));
+    write_setting_b(sesskey, "NoRemoteCharset", conf_get_bool(conf, CONF_no_remote_charset));
+    write_setting_b(sesskey, "ApplicationCursorKeys", conf_get_bool(conf, CONF_app_cursor));
+    write_setting_b(sesskey, "ApplicationKeypad", conf_get_bool(conf, CONF_app_keypad));
+    write_setting_b(sesskey, "NetHackKeypad", conf_get_bool(conf, CONF_nethack_keypad));
+    write_setting_b(sesskey, "AltF4", conf_get_bool(conf, CONF_alt_f4));
+    write_setting_b(sesskey, "AltSpace", conf_get_bool(conf, CONF_alt_space));
+    write_setting_b(sesskey, "AltOnly", conf_get_bool(conf, CONF_alt_only));
+    write_setting_b(sesskey, "ComposeKey", conf_get_bool(conf, CONF_compose_key));
+    write_setting_b(sesskey, "CtrlAltKeys", conf_get_bool(conf, CONF_ctrlaltkeys));
+#ifdef OSX_META_KEY_CONFIG
+    write_setting_b(sesskey, "OSXOptionMeta", conf_get_bool(conf, CONF_osx_option_meta));
+    write_setting_b(sesskey, "OSXCommandMeta", conf_get_bool(conf, CONF_osx_command_meta));
+#endif
+    write_setting_b(sesskey, "TelnetKey", conf_get_bool(conf, CONF_telnet_keyboard));
+    write_setting_b(sesskey, "TelnetRet", conf_get_bool(conf, CONF_telnet_newline));
+    write_setting_i(sesskey, "LocalEcho", conf_get_int(conf, CONF_localecho));
+    write_setting_i(sesskey, "LocalEdit", conf_get_int(conf, CONF_localedit));
+    write_setting_s(sesskey, "Answerback", conf_get_str(conf, CONF_answerback));
+    write_setting_b(sesskey, "AlwaysOnTop", conf_get_bool(conf, CONF_alwaysontop));
+    write_setting_b(sesskey, "FullScreenOnAltEnter", conf_get_bool(conf, CONF_fullscreenonaltenter));
+    write_setting_b(sesskey, "HideMousePtr", conf_get_bool(conf, CONF_hide_mouseptr));
+    write_setting_b(sesskey, "SunkenEdge", conf_get_bool(conf, CONF_sunken_edge));
+    write_setting_i(sesskey, "WindowBorder", conf_get_int(conf, CONF_window_border));
+    write_setting_i(sesskey, "CurType", conf_get_int(conf, CONF_cursor_type));
+    write_setting_b(sesskey, "BlinkCur", conf_get_bool(conf, CONF_blink_cur));
+    write_setting_i(sesskey, "Beep", conf_get_int(conf, CONF_beep));
+    write_setting_i(sesskey, "BeepInd", conf_get_int(conf, CONF_beep_ind));
+    write_setting_filename(sesskey, "BellWaveFile", conf_get_filename(conf, CONF_bell_wavefile));
+    write_setting_b(sesskey, "BellOverload", conf_get_bool(conf, CONF_bellovl));
+    write_setting_i(sesskey, "BellOverloadN", conf_get_int(conf, CONF_bellovl_n));
+    write_setting_i(sesskey, "BellOverloadT", conf_get_int(conf, CONF_bellovl_t)
+#ifdef PUTTY_UNIX_PLATFORM_H
+                    * 1000
+#endif
+                    );
+    write_setting_i(sesskey, "BellOverloadS", conf_get_int(conf, CONF_bellovl_s)
+#ifdef PUTTY_UNIX_PLATFORM_H
+                    * 1000
+#endif
+                    );
+    write_setting_i(sesskey, "ScrollbackLines", conf_get_int(conf, CONF_savelines));
+    write_setting_b(sesskey, "DECOriginMode", conf_get_bool(conf, CONF_dec_om));
+    write_setting_b(sesskey, "AutoWrapMode", conf_get_bool(conf, CONF_wrap_mode));
+    write_setting_b(sesskey, "LFImpliesCR", conf_get_bool(conf, CONF_lfhascr));
+    write_setting_b(sesskey, "CRImpliesLF", conf_get_bool(conf, CONF_crhaslf));
+    write_setting_b(sesskey, "DisableArabicShaping", conf_get_bool(conf, CONF_no_arabicshaping));
+    write_setting_b(sesskey, "DisableBidi", conf_get_bool(conf, CONF_no_bidi));
+    write_setting_b(sesskey, "WinNameAlways", conf_get_bool(conf, CONF_win_name_always));
+    write_setting_s(sesskey, "WinTitle", conf_get_str(conf, CONF_wintitle));
+    write_setting_i(sesskey, "TermWidth", conf_get_int(conf, CONF_width));
+    write_setting_i(sesskey, "TermHeight", conf_get_int(conf, CONF_height));
+    write_setting_fontspec(sesskey, "Font", conf_get_fontspec(conf, CONF_font));
+    write_setting_i(sesskey, "FontQuality", conf_get_int(conf, CONF_font_quality));
+    write_setting_i(sesskey, "FontVTMode", conf_get_int(conf, CONF_vtmode));
+    write_setting_b(sesskey, "UseSystemColours", conf_get_bool(conf, CONF_system_colour));
+    write_setting_b(sesskey, "TryPalette", conf_get_bool(conf, CONF_try_palette));
+    write_setting_b(sesskey, "ANSIColour", conf_get_bool(conf, CONF_ansi_colour));
+    write_setting_b(sesskey, "Xterm256Colour", conf_get_bool(conf, CONF_xterm_256_colour));
+    write_setting_b(sesskey, "TrueColour", conf_get_bool(conf, CONF_true_colour));
+    write_setting_i(sesskey, "BoldAsColour", conf_get_int(conf, CONF_bold_style)-1);
+
+    for (i = 0; i < 22; i++) {
+        char buf[20], buf2[30];
+        sprintf(buf, "Colour%d", i);
+        sprintf(buf2, "%d,%d,%d",
+                conf_get_int_int(conf, CONF_colours, i*3+0),
+                conf_get_int_int(conf, CONF_colours, i*3+1),
+                conf_get_int_int(conf, CONF_colours, i*3+2));
+        write_setting_s(sesskey, buf, buf2);
+    }
+    write_setting_b(sesskey, "RawCNP", conf_get_bool(conf, CONF_rawcnp));
+    write_setting_b(sesskey, "UTF8linedraw", conf_get_bool(conf, CONF_utf8linedraw));
+    write_setting_b(sesskey, "PasteRTF", conf_get_bool(conf, CONF_rtf_paste));
+    write_setting_i(sesskey, "MouseIsXterm", conf_get_int(conf, CONF_mouse_is_xterm));
+    write_setting_b(sesskey, "RectSelect", conf_get_bool(conf, CONF_rect_select));
+    write_setting_b(sesskey, "PasteControls", conf_get_bool(conf, CONF_paste_controls));
+    write_setting_b(sesskey, "MouseOverride", conf_get_bool(conf, CONF_mouse_override));
+    for (i = 0; i < 256; i += 32) {
+        char buf[20], buf2[256];
+        int j;
+        sprintf(buf, "Wordness%d", i);
+        *buf2 = '\0';
+        for (j = i; j < i + 32; j++) {
+            sprintf(buf2 + strlen(buf2), "%s%d",
+                    (*buf2 ? "," : ""),
+                    conf_get_int_int(conf, CONF_wordness, j));
+        }
+        write_setting_s(sesskey, buf, buf2);
+    }
+    write_setting_b(sesskey, "MouseAutocopy",
+                    conf_get_bool(conf, CONF_mouseautocopy));
+    write_clip_setting(sesskey, "MousePaste", conf,
+                       CONF_mousepaste, CONF_mousepaste_custom);
+    write_clip_setting(sesskey, "CtrlShiftIns", conf,
+                       CONF_ctrlshiftins, CONF_ctrlshiftins_custom);
+    write_clip_setting(sesskey, "CtrlShiftCV", conf,
+                       CONF_ctrlshiftcv, CONF_ctrlshiftcv_custom);
+    write_setting_s(sesskey, "LineCodePage", conf_get_str(conf, CONF_line_codepage));
+    write_setting_b(sesskey, "CJKAmbigWide", conf_get_bool(conf, CONF_cjk_ambig_wide));
+    write_setting_b(sesskey, "UTF8Override", conf_get_bool(conf, CONF_utf8_override));
+    write_setting_s(sesskey, "Printer", conf_get_str(conf, CONF_printer));
+    write_setting_b(sesskey, "CapsLockCyr", conf_get_bool(conf, CONF_xlat_capslockcyr));
+    write_setting_b(sesskey, "ScrollBar", conf_get_bool(conf, CONF_scrollbar));
+    write_setting_b(sesskey, "ScrollBarFullScreen", conf_get_bool(conf, CONF_scrollbar_in_fullscreen));
+    write_setting_b(sesskey, "ScrollOnKey", conf_get_bool(conf, CONF_scroll_on_key));
+    write_setting_b(sesskey, "ScrollOnDisp", conf_get_bool(conf, CONF_scroll_on_disp));
+    write_setting_b(sesskey, "EraseToScrollback", conf_get_bool(conf, CONF_erase_to_scrollback));
+    write_setting_i(sesskey, "LockSize", conf_get_int(conf, CONF_resize_action));
+    write_setting_b(sesskey, "BCE", conf_get_bool(conf, CONF_bce));
+    write_setting_b(sesskey, "BlinkText", conf_get_bool(conf, CONF_blinktext));
+    write_setting_b(sesskey, "X11Forward", conf_get_bool(conf, CONF_x11_forward));
+    write_setting_s(sesskey, "X11Display", conf_get_str(conf, CONF_x11_display));
+    write_setting_i(sesskey, "X11AuthType", conf_get_int(conf, CONF_x11_auth));
+    write_setting_filename(sesskey, "X11AuthFile", conf_get_filename(conf, CONF_xauthfile));
+    write_setting_b(sesskey, "LocalPortAcceptAll", conf_get_bool(conf, CONF_lport_acceptall));
+    write_setting_b(sesskey, "RemotePortAcceptAll", conf_get_bool(conf, CONF_rport_acceptall));
+    wmap(sesskey, "PortForwardings", conf, CONF_portfwd, true);
+    write_setting_i(sesskey, "BugIgnore1", 2-conf_get_int(conf, CONF_sshbug_ignore1));
+    write_setting_i(sesskey, "BugPlainPW1", 2-conf_get_int(conf, CONF_sshbug_plainpw1));
+    write_setting_i(sesskey, "BugRSA1", 2-conf_get_int(conf, CONF_sshbug_rsa1));
+    write_setting_i(sesskey, "BugIgnore2", 2-conf_get_int(conf, CONF_sshbug_ignore2));
+    write_setting_i(sesskey, "BugHMAC2", 2-conf_get_int(conf, CONF_sshbug_hmac2));
+    write_setting_i(sesskey, "BugDeriveKey2", 2-conf_get_int(conf, CONF_sshbug_derivekey2));
+    write_setting_i(sesskey, "BugRSAPad2", 2-conf_get_int(conf, CONF_sshbug_rsapad2));
+    write_setting_i(sesskey, "BugPKSessID2", 2-conf_get_int(conf, CONF_sshbug_pksessid2));
+    write_setting_i(sesskey, "BugRekey2", 2-conf_get_int(conf, CONF_sshbug_rekey2));
+    write_setting_i(sesskey, "BugMaxPkt2", 2-conf_get_int(conf, CONF_sshbug_maxpkt2));
+    write_setting_i(sesskey, "BugOldGex2", 2-conf_get_int(conf, CONF_sshbug_oldgex2));
+    write_setting_i(sesskey, "BugWinadj", 2-conf_get_int(conf, CONF_sshbug_winadj));
+    write_setting_i(sesskey, "BugChanReq", 2-conf_get_int(conf, CONF_sshbug_chanreq));
+    write_setting_i(sesskey, "BugRSASHA2CertUserauth", 2-conf_get_int(conf, CONF_sshbug_rsa_sha2_cert_userauth));
+    write_setting_i(sesskey, "BugDropStart", 2-conf_get_int(conf, CONF_sshbug_dropstart));
+    write_setting_i(sesskey, "BugFilterKexinit", 2-conf_get_int(conf, CONF_sshbug_filter_kexinit));
+    write_setting_b(sesskey, "StampUtmp", conf_get_bool(conf, CONF_stamp_utmp));
+    write_setting_b(sesskey, "LoginShell", conf_get_bool(conf, CONF_login_shell));
+    write_setting_b(sesskey, "ScrollbarOnLeft", conf_get_bool(conf, CONF_scrollbar_on_left));
+    write_setting_fontspec(sesskey, "BoldFont", conf_get_fontspec(conf, CONF_boldfont));
+    write_setting_fontspec(sesskey, "WideFont", conf_get_fontspec(conf, CONF_widefont));
+    write_setting_fontspec(sesskey, "WideBoldFont", conf_get_fontspec(conf, CONF_wideboldfont));
+    write_setting_b(sesskey, "ShadowBold", conf_get_bool(conf, CONF_shadowbold));
+    write_setting_i(sesskey, "ShadowBoldOffset", conf_get_int(conf, CONF_shadowboldoffset));
+    write_setting_s(sesskey, "SerialLine", conf_get_str(conf, CONF_serline));
+    write_setting_i(sesskey, "SerialSpeed", conf_get_int(conf, CONF_serspeed));
+    write_setting_i(sesskey, "SerialDataBits", conf_get_int(conf, CONF_serdatabits));
+    write_setting_i(sesskey, "SerialStopHalfbits", conf_get_int(conf, CONF_serstopbits));
+    write_setting_i(sesskey, "SerialParity", conf_get_int(conf, CONF_serparity));
+    write_setting_i(sesskey, "SerialFlowControl", conf_get_int(conf, CONF_serflow));
+    write_setting_s(sesskey, "WindowClass", conf_get_str(conf, CONF_winclass));
+    write_setting_b(sesskey, "ConnectionSharing", conf_get_bool(conf, CONF_ssh_connection_sharing));
+    write_setting_b(sesskey, "ConnectionSharingUpstream", conf_get_bool(conf, CONF_ssh_connection_sharing_upstream));
+    write_setting_b(sesskey, "ConnectionSharingDownstream", conf_get_bool(conf, CONF_ssh_connection_sharing_downstream));
+    wmap(sesskey, "SSHManualHostKeys", conf, CONF_ssh_manual_hostkeys, false);
+
+    /*
+     * SUPDUP settings
+     */
+    write_setting_s(sesskey, "SUPDUPLocation", conf_get_str(conf, CONF_supdup_location));
+    write_setting_i(sesskey, "SUPDUPCharset", conf_get_int(conf, CONF_supdup_ascii_set));
+    write_setting_b(sesskey, "SUPDUPMoreProcessing", conf_get_bool(conf, CONF_supdup_more));
+    write_setting_b(sesskey, "SUPDUPScrolling", conf_get_bool(conf, CONF_supdup_scroll));
+}
+
+bool load_settings(const char *section, Conf *conf)
+{
+    settings_r *sesskey;
+
+    sesskey = open_settings_r(section);
+    bool exists = (sesskey != NULL);
+    load_open_settings(sesskey, conf);
+    close_settings_r(sesskey);
+
+    if (exists && conf_launchable(conf))
+        add_session_to_jumplist(section);
+
+    return exists;
+}
+
+void load_open_settings(settings_r *sesskey, Conf *conf)
+{
+    int i;
+    char *prot;
+
+    conf_set_bool(conf, CONF_ssh_subsys, false); /* FIXME: load this properly */
+    conf_set_str(conf, CONF_remote_cmd, "");
+    conf_set_str(conf, CONF_remote_cmd2, "");
+    conf_set_str(conf, CONF_ssh_nc_host, "");
+
+    gpps(sesskey, "HostName", "", conf, CONF_host);
+    gppfile(sesskey, "LogFileName", conf, CONF_logfilename);
+    gppi(sesskey, "LogType", 0, conf, CONF_logtype);
+    gppi(sesskey, "LogFileClash", LGXF_ASK, conf, CONF_logxfovr);
+    gppb(sesskey, "LogFlush", true, conf, CONF_logflush);
+    gppb(sesskey, "LogHeader", true, conf, CONF_logheader);
+    gppb(sesskey, "SSHLogOmitPasswords", true, conf, CONF_logomitpass);
+    gppb(sesskey, "SSHLogOmitData", false, conf, CONF_logomitdata);
+
+    prot = gpps_raw(sesskey, "Protocol", "default");
+    conf_set_int(conf, CONF_protocol, default_protocol);
+    conf_set_int(conf, CONF_port, default_port);
+    {
+        const struct BackendVtable *vt = backend_vt_from_name(prot);
+        if (vt) {
+            conf_set_int(conf, CONF_protocol, vt->protocol);
+            gppi(sesskey, "PortNumber", default_port, conf, CONF_port);
+        }
+    }
+    sfree(prot);
+
+    /* Address family selection */
+    gppi(sesskey, "AddressFamily", ADDRTYPE_UNSPEC, conf, CONF_addressfamily);
+
+    /* The CloseOnExit numbers are arranged in a different order from
+     * the standard FORCE_ON / FORCE_OFF / AUTO. */
+    i = gppi_raw(sesskey, "CloseOnExit", 1); conf_set_int(conf, CONF_close_on_exit, (i+1)%3);
+    gppb(sesskey, "WarnOnClose", true, conf, CONF_warn_on_close);
+    {
+        /* This is two values for backward compatibility with 0.50/0.51 */
+        int pingmin, pingsec;
+        pingmin = gppi_raw(sesskey, "PingInterval", 0);
+        pingsec = gppi_raw(sesskey, "PingIntervalSecs", 0);
+        conf_set_int(conf, CONF_ping_interval, pingmin * 60 + pingsec);
+    }
+    gppb(sesskey, "TCPNoDelay", true, conf, CONF_tcp_nodelay);
+    gppb(sesskey, "TCPKeepalives", false, conf, CONF_tcp_keepalives);
+    gpps(sesskey, "TerminalType", "xterm", conf, CONF_termtype);
+    gpps(sesskey, "TerminalSpeed", "38400,38400", conf, CONF_termspeed);
+    if (gppmap(sesskey, "TerminalModes", conf, CONF_ttymodes)) {
+        /*
+         * Backwards compatibility with old saved settings.
+         *
+         * From the invention of this setting through 0.67, the set of
+         * terminal modes was fixed, and absence of a mode from this
+         * setting meant the user had explicitly removed it from the
+         * UI and we shouldn't send it.
+         *
+         * In 0.68, the IUTF8 mode was added, and in handling old
+         * settings we inadvertently removed the ability to not send
+         * a mode. Any mode not mentioned was treated as if it was
+         * set to 'auto' (A).
+         *
+         * After 0.68, we added explicit notation to the setting format
+         * when the user removes a known terminal mode from the list.
+         *
+         * So: if any of the modes from the original set is missing, we
+         * assume this was an intentional removal by the user and add
+         * an explicit removal ('N'); but if IUTF8 (or any other mode
+         * added after 0.67) is missing, we assume that its absence is
+         * due to the setting being old rather than intentional, and
+         * add it with its default setting.
+         *
+         * (This does mean that if a 0.68 user explicitly removed IUTF8,
+         * we add it back; but removing IUTF8 had no effect in 0.68, so
+         * we're preserving behaviour, which is the best we can do.)
+         */
+        for (i = 0; ttymodes[i]; i++) {
+            if (!conf_get_str_str_opt(conf, CONF_ttymodes, ttymodes[i])) {
+                /* Mode not mentioned in setting. */
+                const char *def;
+                if (!strcmp(ttymodes[i], "IUTF8")) {
+                    /* Any new modes we add in future should be treated
+                     * this way too. */
+                    def = "A";  /* same as new-setting default below */
+                } else {
+                    /* One of the original modes. Absence is probably
+                     * deliberate. */
+                    def = "N";  /* don't send */
+                }
+                conf_set_str_str(conf, CONF_ttymodes, ttymodes[i], def);
+            }
+        }
+    } else {
+        /* This hardcodes a big set of defaults in any new saved
+         * sessions. Let's hope we don't change our mind. */
+        for (i = 0; ttymodes[i]; i++)
+            conf_set_str_str(conf, CONF_ttymodes, ttymodes[i], "A");
+    }
+
+    /* proxy settings */
+    gpps(sesskey, "ProxyExcludeList", "", conf, CONF_proxy_exclude_list);
+    i = gppi_raw(sesskey, "ProxyDNS", 1); conf_set_int(conf, CONF_proxy_dns, (i+1)%3);
+    gppb(sesskey, "ProxyLocalhost", false, conf, CONF_even_proxy_localhost);
+    gppi(sesskey, "ProxyMethod", -1, conf, CONF_proxy_type);
+    if (conf_get_int(conf, CONF_proxy_type) == -1) {
+        int i;
+        i = gppi_raw(sesskey, "ProxyType", 0);
+        if (i == 0)
+            conf_set_int(conf, CONF_proxy_type, PROXY_NONE);
+        else if (i == 1)
+            conf_set_int(conf, CONF_proxy_type, PROXY_HTTP);
+        else if (i == 3)
+            conf_set_int(conf, CONF_proxy_type, PROXY_TELNET);
+        else if (i == 4)
+            conf_set_int(conf, CONF_proxy_type, PROXY_CMD);
+        else {
+            i = gppi_raw(sesskey, "ProxySOCKSVersion", 5);
+            if (i == 5)
+                conf_set_int(conf, CONF_proxy_type, PROXY_SOCKS5);
+            else
+                conf_set_int(conf, CONF_proxy_type, PROXY_SOCKS4);
+        }
+    }
+    gpps(sesskey, "ProxyHost", "proxy", conf, CONF_proxy_host);
+    gppi(sesskey, "ProxyPort", 80, conf, CONF_proxy_port);
+    gpps(sesskey, "ProxyUsername", "", conf, CONF_proxy_username);
+    gpps(sesskey, "ProxyPassword", "", conf, CONF_proxy_password);
+    gpps(sesskey, "ProxyTelnetCommand", "connect %host %port\\n",
+         conf, CONF_proxy_telnet_command);
+    gppi(sesskey, "ProxyLogToTerm", FORCE_OFF, conf, CONF_proxy_log_to_term);
+    gppmap(sesskey, "Environment", conf, CONF_environmt);
+    gpps(sesskey, "UserName", "", conf, CONF_username);
+    gppb(sesskey, "UserNameFromEnvironment", false,
+         conf, CONF_username_from_env);
+    gpps(sesskey, "LocalUserName", "", conf, CONF_localusername);
+    gppb(sesskey, "NoPTY", false, conf, CONF_nopty);
+    gppb(sesskey, "Compression", false, conf, CONF_compression);
+    gppb(sesskey, "TryAgent", true, conf, CONF_tryagent);
+    gppb(sesskey, "AgentFwd", false, conf, CONF_agentfwd);
+    gppb(sesskey, "ChangeUsername", false, conf, CONF_change_username);
+#ifndef NO_GSSAPI
+    gppb(sesskey, "GssapiFwd", false, conf, CONF_gssapifwd);
+#endif
+    gprefs(sesskey, "Cipher", "\0",
+           ciphernames, CIPHER_MAX, conf, CONF_ssh_cipherlist);
+    {
+        /* Backward-compatibility: before 0.58 (when the "KEX"
+         * preference was first added), we had an option to
+         * disable gex under the "bugs" panel after one report of
+         * a server which offered it then choked, but we never got
+         * a server version string or any other reports. */
+        const char *default_kexes,
+                   *normal_default = "ecdh,dh-gex-sha1,dh-group18-sha512,dh-group17-sha512,dh-group16-sha512,dh-group15-sha512,dh-group14-sha1,rsa,"
+                       "WARN,dh-group1-sha1",
+                   *bugdhgex2_default = "ecdh,dh-group18-sha512,dh-group17-sha512,dh-group16-sha512,dh-group15-sha512,dh-group14-sha1,rsa,"
+                       "WARN,dh-group1-sha1,dh-gex-sha1";
+        char *raw;
+        i = 2 - gppi_raw(sesskey, "BugDHGEx2", 0);
+        if (i == FORCE_ON)
+            default_kexes = bugdhgex2_default;
+        else
+            default_kexes = normal_default;
+        /* Migration: after 0.67 we decided we didn't like
+         * dh-group1-sha1. If it looks like the user never changed
+         * the defaults, quietly upgrade their settings to demote it.
+         * (If they did, they're on their own.) */
+        raw = gpps_raw(sesskey, "KEX", default_kexes);
+        assert(raw != NULL);
+        /* Lack of 'ecdh' tells us this was saved by 0.58-0.67
+         * inclusive. If it was saved by a later version, we need
+         * to leave it alone. */
+        if (strcmp(raw, "dh-group14-sha1,dh-group1-sha1,rsa,"
+                   "WARN,dh-gex-sha1") == 0) {
+            /* Previously migrated from BugDHGEx2. */
+            sfree(raw);
+            raw = dupstr(bugdhgex2_default);
+        } else if (strcmp(raw, "dh-gex-sha1,dh-group14-sha1,"
+                          "dh-group1-sha1,rsa,WARN") == 0) {
+            /* Untouched old default setting. */
+            sfree(raw);
+            raw = dupstr(normal_default);
+        }
+        /* (For the record: after 0.70, the default algorithm list
+         * very briefly contained the string 'gss-sha1-krb5'; this was
+         * never used in any committed version of code, but was left
+         * over from a pre-commit version of GSS key exchange.
+         * Mentioned here as it is remotely possible that it will turn
+         * up in someone's saved settings in future.) */
+
+        gprefs_from_str(raw, kexnames, KEX_MAX, conf, CONF_ssh_kexlist);
+        sfree(raw);
+    }
+    gprefs(sesskey, "HostKey", "ed25519,ecdsa,rsa,dsa,WARN",
+           hknames, HK_MAX, conf, CONF_ssh_hklist);
+    gppb(sesskey, "PreferKnownHostKeys", true, conf, CONF_ssh_prefer_known_hostkeys);
+    gppi(sesskey, "RekeyTime", 60, conf, CONF_ssh_rekey_time);
+#ifndef NO_GSSAPI
+    gppi(sesskey, "GssapiRekey", GSS_DEF_REKEY_MINS, conf, CONF_gssapirekey);
+#endif
+    gpps(sesskey, "RekeyBytes", "1G", conf, CONF_ssh_rekey_data);
+    {
+        /* SSH-2 only by default */
+        int sshprot = gppi_raw(sesskey, "SshProt", 3);
+        /* Old sessions may contain the values corresponding to the fallbacks
+         * we used to allow; migrate them */
+        if (sshprot == 1)      sshprot = 0; /* => "SSH-1 only" */
+        else if (sshprot == 2) sshprot = 3; /* => "SSH-2 only" */
+        conf_set_int(conf, CONF_sshprot, sshprot);
+    }
+    gpps(sesskey, "LogHost", "", conf, CONF_loghost);
+    gppb(sesskey, "SSH2DES", false, conf, CONF_ssh2_des_cbc);
+    gppb(sesskey, "SshNoAuth", false, conf, CONF_ssh_no_userauth);
+    gppb(sesskey, "SshNoTrivialAuth", false, conf, CONF_ssh_no_trivial_userauth);
+    gppb(sesskey, "SshBanner", true, conf, CONF_ssh_show_banner);
+    gppb(sesskey, "AuthTIS", false, conf, CONF_try_tis_auth);
+    gppb(sesskey, "AuthKI", true, conf, CONF_try_ki_auth);
+#ifndef NO_GSSAPI
+    gppb(sesskey, "AuthGSSAPI", true, conf, CONF_try_gssapi_auth);
+    gppb(sesskey, "AuthGSSAPIKEX", true, conf, CONF_try_gssapi_kex);
+    gprefs(sesskey, "GSSLibs", "\0",
+           gsslibkeywords, ngsslibs, conf, CONF_ssh_gsslist);
+    gppfile(sesskey, "GSSCustom", conf, CONF_ssh_gss_custom);
+#endif
+    gppb(sesskey, "SshNoShell", false, conf, CONF_ssh_no_shell);
+    gppfile(sesskey, "PublicKeyFile", conf, CONF_keyfile);
+    gppfile(sesskey, "DetachedCertificate", conf, CONF_detached_cert);
+    gpps(sesskey, "AuthPlugin", "", conf, CONF_auth_plugin);
+    gpps(sesskey, "RemoteCommand", "", conf, CONF_remote_cmd);
+    gppb(sesskey, "RFCEnviron", false, conf, CONF_rfc_environ);
+    gppb(sesskey, "PassiveTelnet", false, conf, CONF_passive_telnet);
+    gppb(sesskey, "BackspaceIsDelete", true, conf, CONF_bksp_is_delete);
+    gppb(sesskey, "RXVTHomeEnd", false, conf, CONF_rxvt_homeend);
+    gppi(sesskey, "LinuxFunctionKeys", 0, conf, CONF_funky_type);
+    gppi(sesskey, "ShiftedArrowKeys", SHARROW_APPLICATION, conf,
+         CONF_sharrow_type);
+    gppb(sesskey, "NoApplicationKeys", false, conf, CONF_no_applic_k);
+    gppb(sesskey, "NoApplicationCursors", false, conf, CONF_no_applic_c);
+    gppb(sesskey, "NoMouseReporting", false, conf, CONF_no_mouse_rep);
+    gppb(sesskey, "NoRemoteResize", false, conf, CONF_no_remote_resize);
+    gppb(sesskey, "NoAltScreen", false, conf, CONF_no_alt_screen);
+    gppb(sesskey, "NoRemoteWinTitle", false, conf, CONF_no_remote_wintitle);
+    gppb(sesskey, "NoRemoteClearScroll", false,
+         conf, CONF_no_remote_clearscroll);
+    {
+        /* Backward compatibility */
+        int no_remote_qtitle = gppi_raw(sesskey, "NoRemoteQTitle", 1);
+        /* We deliberately interpret the old setting of "no response" as
+         * "empty string". This changes the behaviour, but hopefully for
+         * the better; the user can always recover the old behaviour. */
+        gppi(sesskey, "RemoteQTitleAction",
+             no_remote_qtitle ? TITLE_EMPTY : TITLE_REAL,
+             conf, CONF_remote_qtitle_action);
+    }
+    gppb(sesskey, "NoDBackspace", false, conf, CONF_no_dbackspace);
+    gppb(sesskey, "NoRemoteCharset", false, conf, CONF_no_remote_charset);
+    gppb(sesskey, "ApplicationCursorKeys", false, conf, CONF_app_cursor);
+    gppb(sesskey, "ApplicationKeypad", false, conf, CONF_app_keypad);
+    gppb(sesskey, "NetHackKeypad", false, conf, CONF_nethack_keypad);
+    gppb(sesskey, "AltF4", true, conf, CONF_alt_f4);
+    gppb(sesskey, "AltSpace", false, conf, CONF_alt_space);
+    gppb(sesskey, "AltOnly", false, conf, CONF_alt_only);
+    gppb(sesskey, "ComposeKey", false, conf, CONF_compose_key);
+    gppb(sesskey, "CtrlAltKeys", true, conf, CONF_ctrlaltkeys);
+#ifdef OSX_META_KEY_CONFIG
+    gppb(sesskey, "OSXOptionMeta", true, conf, CONF_osx_option_meta);
+    gppb(sesskey, "OSXCommandMeta", false, conf, CONF_osx_command_meta);
+#endif
+    gppb(sesskey, "TelnetKey", false, conf, CONF_telnet_keyboard);
+    gppb(sesskey, "TelnetRet", true, conf, CONF_telnet_newline);
+    gppi(sesskey, "LocalEcho", AUTO, conf, CONF_localecho);
+    gppi(sesskey, "LocalEdit", AUTO, conf, CONF_localedit);
+    gpps(sesskey, "Answerback", "PuTTY", conf, CONF_answerback);
+    gppb(sesskey, "AlwaysOnTop", false, conf, CONF_alwaysontop);
+    gppb(sesskey, "FullScreenOnAltEnter", false,
+         conf, CONF_fullscreenonaltenter);
+    gppb(sesskey, "HideMousePtr", false, conf, CONF_hide_mouseptr);
+    gppb(sesskey, "SunkenEdge", false, conf, CONF_sunken_edge);
+    gppi(sesskey, "WindowBorder", 1, conf, CONF_window_border);
+    gppi(sesskey, "CurType", 0, conf, CONF_cursor_type);
+    gppb(sesskey, "BlinkCur", false, conf, CONF_blink_cur);
+    /* pedantic compiler tells me I can't use conf, CONF_beep as an int * :-) */
+    gppi(sesskey, "Beep", 1, conf, CONF_beep);
+    gppi(sesskey, "BeepInd", 0, conf, CONF_beep_ind);
+    gppfile(sesskey, "BellWaveFile", conf, CONF_bell_wavefile);
+    gppb(sesskey, "BellOverload", true, conf, CONF_bellovl);
+    gppi(sesskey, "BellOverloadN", 5, conf, CONF_bellovl_n);
+    i = gppi_raw(sesskey, "BellOverloadT", 2*TICKSPERSEC
+#ifdef PUTTY_UNIX_PLATFORM_H
+                                   *1000
+#endif
+                                   );
+    conf_set_int(conf, CONF_bellovl_t, i
+#ifdef PUTTY_UNIX_PLATFORM_H
+                 / 1000
+#endif
+                 );
+    i = gppi_raw(sesskey, "BellOverloadS", 5*TICKSPERSEC
+#ifdef PUTTY_UNIX_PLATFORM_H
+                                   *1000
+#endif
+                                   );
+    conf_set_int(conf, CONF_bellovl_s, i
+#ifdef PUTTY_UNIX_PLATFORM_H
+                 / 1000
+#endif
+                 );
+    gppi(sesskey, "ScrollbackLines", 2000, conf, CONF_savelines);
+    gppb(sesskey, "DECOriginMode", false, conf, CONF_dec_om);
+    gppb(sesskey, "AutoWrapMode", true, conf, CONF_wrap_mode);
+    gppb(sesskey, "LFImpliesCR", false, conf, CONF_lfhascr);
+    gppb(sesskey, "CRImpliesLF", false, conf, CONF_crhaslf);
+    gppb(sesskey, "DisableArabicShaping", false, conf, CONF_no_arabicshaping);
+    gppb(sesskey, "DisableBidi", false, conf, CONF_no_bidi);
+    gppb(sesskey, "WinNameAlways", true, conf, CONF_win_name_always);
+    gpps(sesskey, "WinTitle", "", conf, CONF_wintitle);
+    gppi(sesskey, "TermWidth", 80, conf, CONF_width);
+    gppi(sesskey, "TermHeight", 24, conf, CONF_height);
+    gppfont(sesskey, "Font", conf, CONF_font);
+    gppi(sesskey, "FontQuality", FQ_DEFAULT, conf, CONF_font_quality);
+    gppi(sesskey, "FontVTMode", VT_UNICODE, conf, CONF_vtmode);
+    gppb(sesskey, "UseSystemColours", false, conf, CONF_system_colour);
+    gppb(sesskey, "TryPalette", false, conf, CONF_try_palette);
+    gppb(sesskey, "ANSIColour", true, conf, CONF_ansi_colour);
+    gppb(sesskey, "Xterm256Colour", true, conf, CONF_xterm_256_colour);
+    gppb(sesskey, "TrueColour", true, conf, CONF_true_colour);
+    i = gppi_raw(sesskey, "BoldAsColour", 1); conf_set_int(conf, CONF_bold_style, i+1);
+
+    for (i = 0; i < 22; i++) {
+        static const char *const defaults[] = {
+            "187,187,187", "255,255,255", "0,0,0", "85,85,85", "0,0,0",
+            "0,255,0", "0,0,0", "85,85,85", "187,0,0", "255,85,85",
+            "0,187,0", "85,255,85", "187,187,0", "255,255,85", "0,0,187",
+            "85,85,255", "187,0,187", "255,85,255", "0,187,187",
+            "85,255,255", "187,187,187", "255,255,255"
+        };
+        char buf[20], *buf2;
+        int c0, c1, c2;
+        sprintf(buf, "Colour%d", i);
+        buf2 = gpps_raw(sesskey, buf, defaults[i]);
+        if (sscanf(buf2, "%d,%d,%d", &c0, &c1, &c2) == 3) {
+            conf_set_int_int(conf, CONF_colours, i*3+0, c0);
+            conf_set_int_int(conf, CONF_colours, i*3+1, c1);
+            conf_set_int_int(conf, CONF_colours, i*3+2, c2);
+        }
+        sfree(buf2);
+    }
+    gppb(sesskey, "RawCNP", false, conf, CONF_rawcnp);
+    gppb(sesskey, "UTF8linedraw", false, conf, CONF_utf8linedraw);
+    gppb(sesskey, "PasteRTF", false, conf, CONF_rtf_paste);
+    gppi(sesskey, "MouseIsXterm", 0, conf, CONF_mouse_is_xterm);
+    gppb(sesskey, "RectSelect", false, conf, CONF_rect_select);
+    gppb(sesskey, "PasteControls", false, conf, CONF_paste_controls);
+    gppb(sesskey, "MouseOverride", true, conf, CONF_mouse_override);
+    for (i = 0; i < 256; i += 32) {
+        static const char *const defaults[] = {
+            "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0",
+            "0,1,2,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1",
+            "1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,2",
+            "1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1",
+            "1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1",
+            "1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1",
+            "2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2",
+            "2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2"
+        };
+        char buf[20], *buf2, *p;
+        int j;
+        sprintf(buf, "Wordness%d", i);
+        buf2 = gpps_raw(sesskey, buf, defaults[i / 32]);
+        p = buf2;
+        for (j = i; j < i + 32; j++) {
+            char *q = p;
+            while (*p && *p != ',')
+                p++;
+            if (*p == ',')
+                *p++ = '\0';
+            conf_set_int_int(conf, CONF_wordness, j, atoi(q));
+        }
+        sfree(buf2);
+    }
+    gppb(sesskey, "MouseAutocopy", CLIPUI_DEFAULT_AUTOCOPY,
+         conf, CONF_mouseautocopy);
+    read_clip_setting(sesskey, "MousePaste", CLIPUI_DEFAULT_MOUSE,
+                      conf, CONF_mousepaste, CONF_mousepaste_custom);
+    read_clip_setting(sesskey, "CtrlShiftIns", CLIPUI_DEFAULT_INS,
+                      conf, CONF_ctrlshiftins, CONF_ctrlshiftins_custom);
+    read_clip_setting(sesskey, "CtrlShiftCV", CLIPUI_NONE,
+                      conf, CONF_ctrlshiftcv, CONF_ctrlshiftcv_custom);
+    /*
+     * The empty default for LineCodePage will be converted later
+     * into a plausible default for the locale.
+     */
+    gpps(sesskey, "LineCodePage", "", conf, CONF_line_codepage);
+    gppb(sesskey, "CJKAmbigWide", false, conf, CONF_cjk_ambig_wide);
+    gppb(sesskey, "UTF8Override", true, conf, CONF_utf8_override);
+    gpps(sesskey, "Printer", "", conf, CONF_printer);
+    gppb(sesskey, "CapsLockCyr", false, conf, CONF_xlat_capslockcyr);
+    gppb(sesskey, "ScrollBar", true, conf, CONF_scrollbar);
+    gppb(sesskey, "ScrollBarFullScreen", false,
+         conf, CONF_scrollbar_in_fullscreen);
+    gppb(sesskey, "ScrollOnKey", false, conf, CONF_scroll_on_key);
+    gppb(sesskey, "ScrollOnDisp", true, conf, CONF_scroll_on_disp);
+    gppb(sesskey, "EraseToScrollback", true, conf, CONF_erase_to_scrollback);
+    gppi(sesskey, "LockSize", 0, conf, CONF_resize_action);
+    gppb(sesskey, "BCE", true, conf, CONF_bce);
+    gppb(sesskey, "BlinkText", false, conf, CONF_blinktext);
+    gppb(sesskey, "X11Forward", false, conf, CONF_x11_forward);
+    gpps(sesskey, "X11Display", "", conf, CONF_x11_display);
+    gppi(sesskey, "X11AuthType", X11_MIT, conf, CONF_x11_auth);
+    gppfile(sesskey, "X11AuthFile", conf, CONF_xauthfile);
+
+    gppb(sesskey, "LocalPortAcceptAll", false, conf, CONF_lport_acceptall);
+    gppb(sesskey, "RemotePortAcceptAll", false, conf, CONF_rport_acceptall);
+    gppmap(sesskey, "PortForwardings", conf, CONF_portfwd);
+    i = gppi_raw(sesskey, "BugIgnore1", 0); conf_set_int(conf, CONF_sshbug_ignore1, 2-i);
+    i = gppi_raw(sesskey, "BugPlainPW1", 0); conf_set_int(conf, CONF_sshbug_plainpw1, 2-i);
+    i = gppi_raw(sesskey, "BugRSA1", 0); conf_set_int(conf, CONF_sshbug_rsa1, 2-i);
+    i = gppi_raw(sesskey, "BugIgnore2", 0); conf_set_int(conf, CONF_sshbug_ignore2, 2-i);
+    {
+        int i;
+        i = gppi_raw(sesskey, "BugHMAC2", 0); conf_set_int(conf, CONF_sshbug_hmac2, 2-i);
+        if (2-i == AUTO) {
+            i = gppi_raw(sesskey, "BuggyMAC", 0);
+            if (i == 1)
+                conf_set_int(conf, CONF_sshbug_hmac2, FORCE_ON);
+        }
+    }
+    i = gppi_raw(sesskey, "BugDeriveKey2", 0); conf_set_int(conf, CONF_sshbug_derivekey2, 2-i);
+    i = gppi_raw(sesskey, "BugRSAPad2", 0); conf_set_int(conf, CONF_sshbug_rsapad2, 2-i);
+    i = gppi_raw(sesskey, "BugPKSessID2", 0); conf_set_int(conf, CONF_sshbug_pksessid2, 2-i);
+    i = gppi_raw(sesskey, "BugRekey2", 0); conf_set_int(conf, CONF_sshbug_rekey2, 2-i);
+    i = gppi_raw(sesskey, "BugMaxPkt2", 0); conf_set_int(conf, CONF_sshbug_maxpkt2, 2-i);
+    i = gppi_raw(sesskey, "BugOldGex2", 0); conf_set_int(conf, CONF_sshbug_oldgex2, 2-i);
+    i = gppi_raw(sesskey, "BugWinadj", 0); conf_set_int(conf, CONF_sshbug_winadj, 2-i);
+    i = gppi_raw(sesskey, "BugChanReq", 0); conf_set_int(conf, CONF_sshbug_chanreq, 2-i);
+    i = gppi_raw(sesskey, "BugRSASHA2CertUserauth", 0); conf_set_int(conf, CONF_sshbug_rsa_sha2_cert_userauth, 2-i);
+    i = gppi_raw(sesskey, "BugDropStart", 1); conf_set_int(conf, CONF_sshbug_dropstart, 2-i);
+    i = gppi_raw(sesskey, "BugFilterKexinit", 1); conf_set_int(conf, CONF_sshbug_filter_kexinit, 2-i);
+    conf_set_bool(conf, CONF_ssh_simple, false);
+    gppb(sesskey, "StampUtmp", true, conf, CONF_stamp_utmp);
+    gppb(sesskey, "LoginShell", true, conf, CONF_login_shell);
+    gppb(sesskey, "ScrollbarOnLeft", false, conf, CONF_scrollbar_on_left);
+    gppb(sesskey, "ShadowBold", false, conf, CONF_shadowbold);
+    gppfont(sesskey, "BoldFont", conf, CONF_boldfont);
+    gppfont(sesskey, "WideFont", conf, CONF_widefont);
+    gppfont(sesskey, "WideBoldFont", conf, CONF_wideboldfont);
+    gppi(sesskey, "ShadowBoldOffset", 1, conf, CONF_shadowboldoffset);
+    gpps(sesskey, "SerialLine", "", conf, CONF_serline);
+    gppi(sesskey, "SerialSpeed", 9600, conf, CONF_serspeed);
+    gppi(sesskey, "SerialDataBits", 8, conf, CONF_serdatabits);
+    gppi(sesskey, "SerialStopHalfbits", 2, conf, CONF_serstopbits);
+    gppi(sesskey, "SerialParity", SER_PAR_NONE, conf, CONF_serparity);
+    gppi(sesskey, "SerialFlowControl", SER_FLOW_XONXOFF, conf, CONF_serflow);
+    gpps(sesskey, "WindowClass", "", conf, CONF_winclass);
+    gppb(sesskey, "ConnectionSharing", false,
+         conf, CONF_ssh_connection_sharing);
+    gppb(sesskey, "ConnectionSharingUpstream", true,
+         conf, CONF_ssh_connection_sharing_upstream);
+    gppb(sesskey, "ConnectionSharingDownstream", true,
+         conf, CONF_ssh_connection_sharing_downstream);
+    gppmap(sesskey, "SSHManualHostKeys", conf, CONF_ssh_manual_hostkeys);
+
+    /*
+     * SUPDUP settings
+     */
+    gpps(sesskey, "SUPDUPLocation", "The Internet", conf, CONF_supdup_location);
+    gppi(sesskey, "SUPDUPCharset", false, conf, CONF_supdup_ascii_set);
+    gppb(sesskey, "SUPDUPMoreProcessing", false, conf, CONF_supdup_more);
+    gppb(sesskey, "SUPDUPScrolling", false, conf, CONF_supdup_scroll);
+}
+
+bool do_defaults(const char *session, Conf *conf)
+{
+    return load_settings(session, conf);
+}
+
+static int sessioncmp(const void *av, const void *bv)
+{
+    const char *a = *(const char *const *) av;
+    const char *b = *(const char *const *) bv;
+
+    /*
+     * Alphabetical order, except that "Default Settings" is a
+     * special case and comes first.
+     */
+    if (!strcmp(a, "Default Settings"))
+        return -1;                     /* a comes first */
+    if (!strcmp(b, "Default Settings"))
+        return +1;                     /* b comes first */
+    /*
+     * FIXME: perhaps we should ignore the first & in determining
+     * sort order.
+     */
+    return strcmp(a, b);               /* otherwise, compare normally */
+}
+
+bool sesslist_demo_mode = false;
+
+void get_sesslist(struct sesslist *list, bool allocate)
+{
+    int i;
+    char *p;
+    settings_e *handle;
+
+    if (allocate) {
+        strbuf *sb = strbuf_new();
+
+        if (sesslist_demo_mode) {
+            put_asciz(sb, "demo-server");
+            put_asciz(sb, "demo-server-2");
+        } else {
+            if ((handle = enum_settings_start()) != NULL) {
+                while (enum_settings_next(handle, sb))
+                    put_byte(sb, '\0');
+                enum_settings_finish(handle);
+            }
+            put_byte(sb, '\0');
+        }
+
+        list->buffer = strbuf_to_str(sb);
+
+        /*
+         * Now set up the list of sessions. Note that "Default
+         * Settings" must always be claimed to exist, even if it
+         * doesn't really.
+         */
+
+        p = list->buffer;
+        list->nsessions = 1;           /* "Default Settings" counts as one */
+        while (*p) {
+            if (strcmp(p, "Default Settings"))
+                list->nsessions++;
+            while (*p)
+                p++;
+            p++;
+        }
+
+        list->sessions = snewn(list->nsessions + 1, const char *);
+        list->sessions[0] = "Default Settings";
+        p = list->buffer;
+        i = 1;
+        while (*p) {
+            if (strcmp(p, "Default Settings"))
+                list->sessions[i++] = p;
+            while (*p)
+                p++;
+            p++;
+        }
+
+        qsort(list->sessions, i, sizeof(const char *), sessioncmp);
+    } else {
+        sfree(list->buffer);
+        sfree(list->sessions);
+        list->buffer = NULL;
+        list->sessions = NULL;
+    }
+}
diff --git putty-0.81/terminal/terminal.c putty-url-0.81/terminal/terminal.c
index 90c2880..f02c740 100644
--- putty-0.81/terminal/terminal.c
+++ putty-url-0.81/terminal/terminal.c
@@ -13,6 +13,9 @@
 #include "putty.h"
 #include "terminal.h"
 
+/* HACK: PuTTY-url */
+#include "urlhack.h"
+
 #define VT52_PLUS
 
 #define CL_ANSIMIN      0x0001         /* Codes in all ANSI like terminals. */
@@ -2075,6 +2078,11 @@ Terminal *term_init(Conf *myconf, struct unicode_data *ucsdata, TermWin *win)
     term->bidi_cache_size = 0;
     term->pre_bidi_cache = term->post_bidi_cache = NULL;
 
+	/*
+	 * HACK: PuttyTray / Nutty
+	 */
+	term->url_update = TRUE;
+
     /* FULL-TERMCHAR */
     term->basic_erase_char.chr = CSET_ASCII | ' ';
     term->basic_erase_char.attr = ATTR_DEFAULT;
@@ -6046,6 +6054,50 @@ static void do_paint(Terminal *term)
     size_t chlen;
     termchar *newline;
 
+    /*
+     * HACK: PuttyTray / Nutty
+     * Hyperlink stuff: Find visible hyperlinks
+     *
+     * TODO: We should find out somehow that the stuff on screen has changed since last
+     *       paint. How to do it?
+     */
+    int urlhack_underline_always = conf_get_int(term->conf, CONF_url_underline) == URLHACK_UNDERLINE_ALWAYS;
+
+    int urlhack_underline =
+        conf_get_int(term->conf, CONF_url_underline) == URLHACK_UNDERLINE_ALWAYS ||
+        (conf_get_int(term->conf, CONF_url_underline) == URLHACK_UNDERLINE_HOVER && (!conf_get_bool(term->conf, CONF_url_ctrl_click) || urlhack_is_ctrl_pressed())) ? 1 : 0;
+
+    int urlhack_is_link = 0, urlhack_hover_current = 0;
+    int urlhack_toggle_x = term->cols, urlhack_toggle_y = term->rows;
+    int urlhack_region_index = 0;
+    text_region urlhack_region;
+
+    if (term->url_update) {
+        urlhack_reset();
+
+        for (i = 0; i < term->rows; i++) {
+            termline *lp = lineptr(term->disptop + i);
+
+            for (j = 0; j < term->cols; j++) {
+                unsigned long termchr = lp->chars[j].chr;
+                urlhack_putchar(termchr & CHAR_MASK ? (char)(termchr & CHAR_MASK) : ' ');
+            }
+
+            unlineptr(lp);
+        }
+
+        urlhack_go_find_me_some_hyperlinks(term->cols);
+    }
+    urlhack_region = urlhack_get_link_region(urlhack_region_index);
+    urlhack_toggle_x = urlhack_region.x0;
+    urlhack_toggle_y = urlhack_region.y0;
+
+    if (urlhack_underline_always)
+        urlhack_hover_current = 1;
+    else
+        urlhack_hover_current = urlhack_is_in_this_link_region(urlhack_region, urlhack_mouse_old_x, urlhack_mouse_old_y);
+    /* HACK: PuttyTray / Nutty : END */
+
     chlen = 1024;
     ch = snewn(chlen, wchar_t);
 
@@ -6195,6 +6247,47 @@ static void do_paint(Terminal *term)
             if (j < term->cols-1 && d[1].chr == UCSWIDE)
                 tattr |= ATTR_WIDE;
 
+            /*
+             * HACK: PuTTY-url
+             * Hyperlink stuff: Underline link regions if user has configured us so
+             */
+            if (urlhack_underline) {
+                if (j == urlhack_toggle_x && i == urlhack_toggle_y) {
+                    urlhack_is_link = urlhack_is_link == 1 ? 0 : 1;
+
+                    // Find next bound for the toggle
+
+                    if (urlhack_is_link == 1) {
+                        urlhack_toggle_x = urlhack_region.x1;
+                        urlhack_toggle_y = urlhack_region.y1;
+
+                        if (urlhack_toggle_x == term->cols - 1) {
+                            // Handle special case where link ends at the last char of the row
+                            urlhack_toggle_y++;
+                            urlhack_toggle_x = 0;
+                        }
+                    } else {
+                        urlhack_region = urlhack_get_link_region(++urlhack_region_index);
+
+                        if (urlhack_underline_always)
+                            urlhack_hover_current = 1;
+                        else
+                            urlhack_hover_current = urlhack_is_in_this_link_region(urlhack_region, urlhack_mouse_old_x, urlhack_mouse_old_y);
+
+                        urlhack_toggle_x = urlhack_region.x0;
+                        urlhack_toggle_y = urlhack_region.y0;
+                    }
+                }
+
+                if (urlhack_is_link == 1 && urlhack_hover_current == 1) {
+                    tattr |= ATTR_UNDER;
+                }
+
+                term->url_update = 0;
+            }
+            /* HACK: PuttyTray / Nutty : END */
+
+
             /* Video reversing things */
             if (term->selstate == DRAGGING || term->selstate == SELECTED) {
                 if (term->seltype == LEXICOGRAPHIC)
@@ -6500,6 +6593,11 @@ void term_scroll(Terminal *term, int rel, int where)
 {
     int sbtop = -sblines(term);
 
+	/*
+	 * HACK: PuttyTray / Nutty
+	 */
+	term->url_update = TRUE;
+
     term->disptop = (rel < 0 ? 0 : rel > 0 ? sbtop : term->disptop) + where;
     if (term->disptop < sbtop)
         term->disptop = sbtop;
@@ -7242,7 +7340,7 @@ void term_mouse(Terminal *term, Mouse_Button braw, Mouse_Button bcooked,
     }
 
     selpoint.x = x;
-    unlineptr(ldata);
+    //unlineptr(ldata); // HACK: REMOVED FOR HYPERLINK STUFF
 
     /*
      * If we're in the middle of a selection operation, we ignore raw
@@ -7307,8 +7405,10 @@ void term_mouse(Terminal *term, Mouse_Button braw, Mouse_Button bcooked,
                     return;
             } else switch (a) {
               case MA_DRAG:
-                if (term->xterm_mouse == 1)
+                if (term->xterm_mouse == 1) {
+                    unlineptr(ldata); /* HACK: ADDED FOR hyperlink stuff */
                     return;
+                }
                 encstate += 0x20; // motion indicator
                 break;
               case MA_MOVE:    // mouse move without buttons
@@ -7332,8 +7432,10 @@ void term_mouse(Terminal *term, Mouse_Button braw, Mouse_Button bcooked,
                 term->mouse_is_down = 0;
                 break;
               case MA_CLICK:
-                if (term->mouse_is_down == braw)
+                if (term->mouse_is_down == braw) {
+                    unlineptr(ldata); /* HACK: ADDED FOR hyperlink stuff */
                     return;
+                }
                 term->mouse_is_down = braw;
                 break;
               default:
@@ -7357,6 +7459,7 @@ void term_mouse(Terminal *term, Mouse_Button braw, Mouse_Button bcooked,
             if (len > 0)
                 ldisc_send(term->ldisc, abuf, len, false);
         }
+        unlineptr(ldata); /* HACK: ADDED FOR hyperlink stuff */
         return;
     }
 
@@ -7379,6 +7482,60 @@ void term_mouse(Terminal *term, Mouse_Button braw, Mouse_Button bcooked,
         term->seltype = default_seltype;
         term->selanchor = selpoint;
         term->selmode = SM_CHAR;
+
+
+        /*
+         * HACK: PuTTY-url
+         * Hyperlink stuff: Check whether the click coordinates are inside link
+         * region, if so -> copy url to temporary buffer and launch it. Delete
+         * the temporary buffer.
+         */
+    } else if (bcooked == MBT_SELECT && a == MA_RELEASE && term->selstate == ABOUT_TO) {
+        deselect(term);
+        term->selstate = NO_SELECTION;
+
+        if ((!conf_get_bool(term->conf, CONF_url_ctrl_click) || (conf_get_bool(term->conf, CONF_url_ctrl_click) && urlhack_is_ctrl_pressed())) && urlhack_is_in_link_region(x, y)) {
+            int i;
+            char *linkbuf = NULL;
+            text_region region = urlhack_get_link_bounds(x, y);
+
+            if (region.y0 == region.y1) {
+                linkbuf = snewn(region.x1 - region.x0 + 2, char);
+
+                for (i = region.x0; i < region.x1; i++) {
+                    linkbuf[i - region.x0] = (char)(ldata->chars[i].chr);
+                }
+
+                linkbuf[i - region.x0] = '\0';
+            }
+            else {
+                termline *urldata = lineptr(region.y0 + term->disptop);
+                int linklen, row = region.y0 + term->disptop;
+
+                linklen = (term->cols - region.x0) +
+                    ((region.y1 - region.y0 - 1) * term->cols) + region.x1 + 1;
+
+                linkbuf = snewn(linklen, char);
+
+                for (i = region.x0; i < linklen + region.x0; i++) {
+                    linkbuf[i - region.x0] = (char)(urldata->chars[i % term->cols].chr);
+
+                    // Jump to next line?
+                    if (((i + 1) % term->cols) == 0) {
+                        row++;
+                        urldata = lineptr(row);
+                    }
+                }
+
+                linkbuf[linklen - 1] = '\0';
+                unlineptr(urldata);
+            }
+
+            urlhack_launch_url(!conf_get_bool(term->conf, CONF_url_defbrowser) ? conf_get_filename(term->conf, CONF_url_browser)->path : NULL, linkbuf);
+
+            sfree(linkbuf);
+        }
+        /* HACK: PuttyTray / Nutty : END */
     } else if (bcooked == MBT_SELECT && (a == MA_2CLK || a == MA_3CLK)) {
         deselect(term);
         term->selmode = (a == MA_2CLK ? SM_WORD : SM_LINE);
@@ -7402,8 +7559,10 @@ void term_mouse(Terminal *term, Mouse_Button braw, Mouse_Button bcooked,
              */
             return;
         }
-        if (term->selstate == ABOUT_TO && poseq(term->selanchor, selpoint))
+        if (term->selstate == ABOUT_TO && poseq(term->selanchor, selpoint)) {
+            unlineptr(ldata);
             return;
+        }
         if (bcooked == MBT_EXTEND && a != MA_DRAG &&
             term->selstate == SELECTED) {
             if (term->seltype == LEXICOGRAPHIC) {
@@ -7845,6 +8004,8 @@ static void term_added_data(Terminal *term, bool called_from_term_data)
         term->in_term_out = true;
         term_out(term, called_from_term_data);
         term->in_term_out = false;
+
+        term->url_update = TRUE; /* HACK: PuTTY-url */
     }
 }
 
diff --git putty-url-0.81/terminal/terminal.c.orig putty-url-0.81/terminal/terminal.c.orig
new file mode 100644
index 0000000..90c2880
--- /dev/null
+++ putty-url-0.81/terminal/terminal.c.orig
@@ -0,0 +1,8077 @@
+/*
+ * Terminal emulator.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <limits.h>
+#include <wchar.h>
+
+#include <time.h>
+#include <assert.h>
+#include "putty.h"
+#include "terminal.h"
+
+#define VT52_PLUS
+
+#define CL_ANSIMIN      0x0001         /* Codes in all ANSI like terminals. */
+#define CL_VT100        0x0002         /* VT100 */
+#define CL_VT100AVO     0x0004         /* VT100 +AVO; 132x24 (not 132x14) & attrs */
+#define CL_VT102        0x0008         /* VT102 */
+#define CL_VT220        0x0010         /* VT220 */
+#define CL_VT320        0x0020         /* VT320 */
+#define CL_VT420        0x0040         /* VT420 */
+#define CL_VT510        0x0080         /* VT510, NB VT510 includes ANSI */
+#define CL_VT340TEXT    0x0100         /* VT340 extensions that appear in the VT420 */
+#define CL_SCOANSI      0x1000         /* SCOANSI not in ANSIMIN. */
+#define CL_ANSI         0x2000         /* ANSI ECMA-48 not in the VT100..VT420 */
+#define CL_OTHER        0x4000         /* Others, Xterm, linux, putty, dunno, etc */
+
+#define TM_VT100        (CL_ANSIMIN|CL_VT100)
+#define TM_VT100AVO     (TM_VT100|CL_VT100AVO)
+#define TM_VT102        (TM_VT100AVO|CL_VT102)
+#define TM_VT220        (TM_VT102|CL_VT220)
+#define TM_VTXXX        (TM_VT220|CL_VT340TEXT|CL_VT510|CL_VT420|CL_VT320)
+#define TM_SCOANSI      (CL_ANSIMIN|CL_SCOANSI)
+
+#define TM_PUTTY        (0xFFFF)
+
+#define UPDATE_DELAY    ((TICKSPERSEC+49)/50)/* ticks to defer window update */
+#define TBLINK_DELAY    ((TICKSPERSEC*9+19)/20)/* ticks between text blinks*/
+#define CBLINK_DELAY    (CURSORBLINK) /* ticks between cursor blinks */
+#define VBELL_DELAY     (VBELL_TIMEOUT) /* visual bell timeout in ticks */
+
+#define compatibility(x) \
+    if ( ((CL_##x)&term->compatibility_level) == 0 ) {  \
+       term->termstate=TOPLEVEL;                        \
+       break;                                           \
+    }
+#define compatibility2(x,y) \
+    if ( ((CL_##x|CL_##y)&term->compatibility_level) == 0 ) { \
+       term->termstate=TOPLEVEL;                        \
+       break;                                           \
+    }
+
+#define has_compat(x) ( ((CL_##x)&term->compatibility_level) != 0 )
+
+static const char *const EMPTY_WINDOW_TITLE = "";
+
+static const char sco2ansicolour[] = { 0, 4, 2, 6, 1, 5, 3, 7 };
+
+#define sel_nl_sz  (sizeof(sel_nl)/sizeof(wchar_t))
+static const wchar_t sel_nl[] = SEL_NL;
+
+/*
+ * Fetch the character at a particular position in a line array,
+ * for purposes of `wordtype'. The reason this isn't just a simple
+ * array reference is that if the character we find is UCSWIDE,
+ * then we must look one space further to the left.
+ */
+#define UCSGET(a, x) \
+    ( (x)>0 && (a)[(x)].chr == UCSWIDE ? (a)[(x)-1].chr : (a)[(x)].chr )
+
+/*
+ * Detect the various aliases of U+0020 SPACE.
+ */
+#define IS_SPACE_CHR(chr) \
+        ((chr) == 0x20 || (DIRECT_CHAR(chr) && ((chr) & 0xFF) == 0x20))
+
+/*
+ * Spot magic CSETs.
+ */
+#define CSET_OF(chr) (DIRECT_CHAR(chr)||DIRECT_FONT(chr) ? (chr)&CSET_MASK : 0)
+
+/*
+ * Internal prototypes.
+ */
+static void resizeline(Terminal *, termline *, int);
+static termline *lineptr(Terminal *, int, int, int);
+static void unlineptr(termline *);
+static void check_line_size(Terminal *, termline *);
+static void do_paint(Terminal *);
+static void erase_lots(Terminal *, bool, bool, bool);
+static int find_last_nonempty_line(Terminal *, tree234 *);
+static void swap_screen(Terminal *, int, bool, bool);
+static void update_sbar(Terminal *);
+static void deselect(Terminal *);
+static void term_print_finish(Terminal *);
+static void scroll(Terminal *, int, int, int, bool);
+static void parse_optionalrgb(optionalrgb *out, unsigned *values);
+static void term_added_data(Terminal *term, bool);
+static void term_update_raw_mouse_mode(Terminal *term);
+static void term_out_cb(void *);
+
+static termline *newtermline(Terminal *term, int cols, bool bce)
+{
+    termline *line;
+    int j;
+
+    line = snew(termline);
+    line->chars = snewn(cols, termchar);
+    for (j = 0; j < cols; j++)
+        line->chars[j] = (bce ? term->erase_char : term->basic_erase_char);
+    line->cols = line->size = cols;
+    line->lattr = LATTR_NORM;
+    line->trusted = false;
+    line->temporary = false;
+    line->cc_free = 0;
+
+    return line;
+}
+
+static void freetermline(termline *line)
+{
+    if (line) {
+        sfree(line->chars);
+        sfree(line);
+    }
+}
+
+static void unlineptr(termline *line)
+{
+    if (line->temporary)
+        freetermline(line);
+}
+
+const int colour_indices_conf_to_oscp[CONF_NCOLOURS] = {
+    #define COLOUR_ENTRY(id,name) OSCP_COLOUR_##id,
+    CONF_COLOUR_LIST(COLOUR_ENTRY)
+    #undef COLOUR_ENTRY
+};
+
+const int colour_indices_conf_to_osc4[CONF_NCOLOURS] = {
+    #define COLOUR_ENTRY(id,name) OSC4_COLOUR_##id,
+    CONF_COLOUR_LIST(COLOUR_ENTRY)
+    #undef COLOUR_ENTRY
+};
+
+const int colour_indices_oscp_to_osc4[OSCP_NCOLOURS] = {
+    #define COLOUR_ENTRY(id) OSC4_COLOUR_##id,
+    OSCP_COLOUR_LIST(COLOUR_ENTRY)
+    #undef COLOUR_ENTRY
+};
+
+#ifdef TERM_CC_DIAGS
+/*
+ * Diagnostic function: verify that a termline has a correct
+ * combining character structure.
+ *
+ * This is a performance-intensive check, so it's no longer enabled
+ * by default.
+ */
+static void cc_check(termline *line)
+{
+    unsigned char *flags;
+    int i, j;
+
+    assert(line->size >= line->cols);
+
+    flags = snewn(line->size, unsigned char);
+
+    for (i = 0; i < line->size; i++)
+        flags[i] = (i < line->cols);
+
+    for (i = 0; i < line->cols; i++) {
+        j = i;
+        while (line->chars[j].cc_next) {
+            j += line->chars[j].cc_next;
+            assert(j >= line->cols && j < line->size);
+            assert(!flags[j]);
+            flags[j] = true;
+        }
+    }
+
+    j = line->cc_free;
+    if (j) {
+        while (1) {
+            assert(j >= line->cols && j < line->size);
+            assert(!flags[j]);
+            flags[j] = true;
+            if (line->chars[j].cc_next)
+                j += line->chars[j].cc_next;
+            else
+                break;
+        }
+    }
+
+    j = 0;
+    for (i = 0; i < line->size; i++)
+        j += (flags[i] != 0);
+
+    assert(j == line->size);
+
+    sfree(flags);
+}
+#endif
+
+static void clear_cc(termline *line, int col);
+
+/*
+ * Add a combining character to a character cell.
+ */
+static void add_cc(termline *line, int col, unsigned long chr)
+{
+    int newcc;
+
+    assert(col >= 0 && col < line->cols);
+
+    /*
+     * Don't add combining characters at all to U+FFFD REPLACEMENT
+     * CHARACTER. (Partly it's a slightly incoherent idea in the first
+     * place; mostly, U+FFFD is what we generate if a cell already has
+     * too many ccs, in which case we want it to be a fixed point when
+     * further ccs are added.)
+     */
+    if (line->chars[col].chr == 0xFFFD)
+        return;
+
+    /*
+     * Walk the cc list of the cell in question to find its current
+     * end point.
+     */
+    size_t ncc = 0;
+    int origcol = col;
+    while (line->chars[col].cc_next) {
+        col += line->chars[col].cc_next;
+        if (++ncc >= CC_LIMIT) {
+            /*
+             * There are already too many combining characters in this
+             * character cell. Change strategy: throw out the entire
+             * chain and replace the main character with U+FFFD.
+             *
+             * (Rationale: extrapolating from UTR #36 section 3.6.2
+             * suggests the principle that it's better to substitute
+             * U+FFFD than to _ignore_ input completely. Also, if the
+             * user copies and pastes an overcombined character cell,
+             * this way it will clearly indicate that we haven't
+             * reproduced the writer's original intentions, instead of
+             * looking as if it was the _writer's_ fault that the 33rd
+             * cc is missing.)
+             *
+             * Per the code above, this will also prevent any further
+             * ccs from being added to this cell.
+             */
+            clear_cc(line, origcol);
+            line->chars[origcol].chr = 0xFFFD;
+            return;
+        }
+    }
+
+    /*
+     * Extend the cols array if the free list is empty.
+     */
+    if (!line->cc_free) {
+        int n = line->size;
+
+        size_t tmpsize = line->size;
+        sgrowarray(line->chars, tmpsize, tmpsize);
+        assert(tmpsize <= INT_MAX);
+        line->size = tmpsize;
+
+        line->cc_free = n;
+        while (n < line->size) {
+            if (n+1 < line->size)
+                line->chars[n].cc_next = 1;
+            else
+                line->chars[n].cc_next = 0;
+            n++;
+        }
+    }
+
+    /*
+     * `col' now points at the last cc currently in this cell; so
+     * we simply add another one.
+     */
+    newcc = line->cc_free;
+    if (line->chars[newcc].cc_next)
+        line->cc_free = newcc + line->chars[newcc].cc_next;
+    else
+        line->cc_free = 0;
+    line->chars[newcc].cc_next = 0;
+    line->chars[newcc].chr = chr;
+    line->chars[col].cc_next = newcc - col;
+
+#ifdef TERM_CC_DIAGS
+    cc_check(line);
+#endif
+}
+
+/*
+ * Clear the combining character list in a character cell.
+ */
+static void clear_cc(termline *line, int col)
+{
+    int oldfree, origcol = col;
+
+    assert(col >= 0 && col < line->cols);
+
+    if (!line->chars[col].cc_next)
+        return;                        /* nothing needs doing */
+
+    oldfree = line->cc_free;
+    line->cc_free = col + line->chars[col].cc_next;
+    while (line->chars[col].cc_next)
+        col += line->chars[col].cc_next;
+    if (oldfree)
+        line->chars[col].cc_next = oldfree - col;
+    else
+        line->chars[col].cc_next = 0;
+
+    line->chars[origcol].cc_next = 0;
+
+#ifdef TERM_CC_DIAGS
+    cc_check(line);
+#endif
+}
+
+/*
+ * Compare two character cells for equality. Special case required
+ * in do_paint() where we override what we expect the chr and attr
+ * fields to be.
+ */
+static bool termchars_equal_override(termchar *a, termchar *b,
+                                     unsigned long bchr, unsigned long battr)
+{
+    /* FULL-TERMCHAR */
+    if (!truecolour_equal(a->truecolour, b->truecolour))
+        return false;
+    if (a->chr != bchr)
+        return false;
+    if ((a->attr &~ DATTR_MASK) != (battr &~ DATTR_MASK))
+        return false;
+    while (a->cc_next || b->cc_next) {
+        if (!a->cc_next || !b->cc_next)
+            return false;              /* one cc-list ends, other does not */
+        a += a->cc_next;
+        b += b->cc_next;
+        if (a->chr != b->chr)
+            return false;
+    }
+    return true;
+}
+
+static bool termchars_equal(termchar *a, termchar *b)
+{
+    return termchars_equal_override(a, b, b->chr, b->attr);
+}
+
+/*
+ * Copy a character cell. (Requires a pointer to the destination
+ * termline, so as to access its free list.)
+ */
+static void copy_termchar(termline *destline, int x, termchar *src)
+{
+    clear_cc(destline, x);
+
+    destline->chars[x] = *src;         /* copy everything except cc-list */
+    destline->chars[x].cc_next = 0;    /* and make sure this is zero */
+
+    while (src->cc_next) {
+        src += src->cc_next;
+        add_cc(destline, x, src->chr);
+    }
+
+#ifdef TERM_CC_DIAGS
+    cc_check(destline);
+#endif
+}
+
+/*
+ * Move a character cell within its termline.
+ */
+static void move_termchar(termline *line, termchar *dest, termchar *src)
+{
+    /* First clear the cc list from the original char, just in case. */
+    clear_cc(line, dest - line->chars);
+
+    /* Move the character cell and adjust its cc_next. */
+    *dest = *src;                      /* copy everything except cc-list */
+    if (src->cc_next)
+        dest->cc_next = src->cc_next - (dest-src);
+
+    /* Ensure the original cell doesn't have a cc list. */
+    src->cc_next = 0;
+
+#ifdef TERM_CC_DIAGS
+    cc_check(line);
+#endif
+}
+
+#ifndef NO_SCROLLBACK_COMPRESSION
+
+/*
+ * Compress and decompress a termline into an RLE-based format for
+ * storing in scrollback. (Since scrollback almost never needs to
+ * be modified and exists in huge quantities, this is a sensible
+ * tradeoff, particularly since it allows us to continue adding
+ * features to the main termchar structure without proportionally
+ * bloating the terminal emulator's memory footprint unless those
+ * features are in constant use.)
+ */
+static void makerle(strbuf *b, termline *ldata,
+                    void (*makeliteral)(strbuf *b, termchar *c,
+                                        unsigned long *state))
+{
+    int hdrpos, hdrsize, n, prevlen, prevpos, thislen, thispos;
+    bool prev2;
+    termchar *c = ldata->chars;
+    unsigned long state = 0, oldstate;
+
+    n = ldata->cols;
+
+    hdrpos = b->len;
+    hdrsize = 0;
+    put_byte(b, 0);
+    prevlen = prevpos = 0;
+    prev2 = false;
+
+    while (n-- > 0) {
+        thispos = b->len;
+        makeliteral(b, c++, &state);
+        thislen = b->len - thispos;
+        if (thislen == prevlen &&
+            !memcmp(b->u + prevpos, b->u + thispos, thislen)) {
+            /*
+             * This literal precisely matches the previous one.
+             * Turn it into a run if it's worthwhile.
+             *
+             * With one-byte literals, it costs us two bytes to
+             * encode a run, plus another byte to write the header
+             * to resume normal output; so a three-element run is
+             * neutral, and anything beyond that is unconditionally
+             * worthwhile. With two-byte literals or more, even a
+             * 2-run is a win.
+             */
+            if (thislen > 1 || prev2) {
+                int runpos, runlen;
+
+                /*
+                 * It's worth encoding a run. Start at prevpos,
+                 * unless hdrsize==0 in which case we can back up
+                 * another one and start by overwriting hdrpos.
+                 */
+
+                hdrsize--;             /* remove the literal at prevpos */
+                if (prev2) {
+                    assert(hdrsize > 0);
+                    hdrsize--;
+                    prevpos -= prevlen;/* and possibly another one */
+                }
+
+                if (hdrsize == 0) {
+                    assert(prevpos == hdrpos + 1);
+                    runpos = hdrpos;
+                    strbuf_shrink_to(b, prevpos+prevlen);
+                } else {
+                    memmove(b->u + prevpos+1, b->u + prevpos, prevlen);
+                    runpos = prevpos;
+                    strbuf_shrink_to(b, prevpos+prevlen+1);
+                    /*
+                     * Terminate the previous run of ordinary
+                     * literals.
+                     */
+                    assert(hdrsize >= 1 && hdrsize <= 128);
+                    b->u[hdrpos] = hdrsize - 1;
+                }
+
+                runlen = prev2 ? 3 : 2;
+
+                while (n > 0 && runlen < 129) {
+                    int tmppos, tmplen;
+                    tmppos = b->len;
+                    oldstate = state;
+                    makeliteral(b, c, &state);
+                    tmplen = b->len - tmppos;
+                    bool match = tmplen == thislen &&
+                        !memcmp(b->u + runpos+1, b->u + tmppos, tmplen);
+                    strbuf_shrink_to(b, tmppos);
+                    if (!match) {
+                        state = oldstate;
+                        break;         /* run over */
+                    }
+                    n--, c++, runlen++;
+                }
+
+                assert(runlen >= 2 && runlen <= 129);
+                b->u[runpos] = runlen + 0x80 - 2;
+
+                hdrpos = b->len;
+                hdrsize = 0;
+                put_byte(b, 0);
+                /* And ensure this run doesn't interfere with the next. */
+                prevlen = prevpos = 0;
+                prev2 = false;
+
+                continue;
+            } else {
+                /*
+                 * Just flag that the previous two literals were
+                 * identical, in case we find a third identical one
+                 * we want to turn into a run.
+                 */
+                prev2 = true;
+                prevlen = thislen;
+                prevpos = thispos;
+            }
+        } else {
+            prev2 = false;
+            prevlen = thislen;
+            prevpos = thispos;
+        }
+
+        /*
+         * This character isn't (yet) part of a run. Add it to
+         * hdrsize.
+         */
+        hdrsize++;
+        if (hdrsize == 128) {
+            b->u[hdrpos] = hdrsize - 1;
+            hdrpos = b->len;
+            hdrsize = 0;
+            put_byte(b, 0);
+            prevlen = prevpos = 0;
+            prev2 = false;
+        }
+    }
+
+    /*
+     * Clean up.
+     */
+    if (hdrsize > 0) {
+        assert(hdrsize <= 128);
+        b->u[hdrpos] = hdrsize - 1;
+    } else {
+        strbuf_shrink_to(b, hdrpos);
+    }
+}
+static void makeliteral_chr(strbuf *b, termchar *c, unsigned long *state)
+{
+    /*
+     * My encoding for characters is UTF-8-like, in that it stores
+     * 7-bit ASCII in one byte and uses high-bit-set bytes as
+     * introducers to indicate a longer sequence. However, it's
+     * unlike UTF-8 in that it doesn't need to be able to
+     * resynchronise, and therefore I don't want to waste two bits
+     * per byte on having recognisable continuation characters.
+     * Also I don't want to rule out the possibility that I may one
+     * day use values 0x80000000-0xFFFFFFFF for interesting
+     * purposes, so unlike UTF-8 I need a full 32-bit range.
+     * Accordingly, here is my encoding:
+     *
+     * 00000000-0000007F: 0xxxxxxx (but see below)
+     * 00000080-00003FFF: 10xxxxxx xxxxxxxx
+     * 00004000-001FFFFF: 110xxxxx xxxxxxxx xxxxxxxx
+     * 00200000-0FFFFFFF: 1110xxxx xxxxxxxx xxxxxxxx xxxxxxxx
+     * 10000000-FFFFFFFF: 11110ZZZ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
+     *
+     * (`Z' is like `x' but is always going to be zero since the
+     * values I'm encoding don't go above 2^32. In principle the
+     * five-byte form of the encoding could extend to 2^35, and
+     * there could be six-, seven-, eight- and nine-byte forms as
+     * well to allow up to 64-bit values to be encoded. But that's
+     * completely unnecessary for these purposes!)
+     *
+     * The encoding as written above would be very simple, except
+     * that 7-bit ASCII can occur in several different ways in the
+     * terminal data; sometimes it crops up in the D800 page
+     * (CSET_ASCII) but at other times it's in the 0000 page (real
+     * Unicode). Therefore, this encoding is actually _stateful_:
+     * the one-byte encoding of 00-7F actually indicates `reuse the
+     * upper three bytes of the last character', and to encode an
+     * absolute value of 00-7F you need to use the two-byte form
+     * instead.
+     */
+    if ((c->chr & ~0x7F) == *state) {
+        put_byte(b, (unsigned char)(c->chr & 0x7F));
+    } else if (c->chr < 0x4000) {
+        put_byte(b, (unsigned char)(((c->chr >> 8) & 0x3F) | 0x80));
+        put_byte(b, (unsigned char)(c->chr & 0xFF));
+    } else if (c->chr < 0x200000) {
+        put_byte(b, (unsigned char)(((c->chr >> 16) & 0x1F) | 0xC0));
+        put_uint16(b, c->chr & 0xFFFF);
+    } else if (c->chr < 0x10000000) {
+        put_byte(b, (unsigned char)(((c->chr >> 24) & 0x0F) | 0xE0));
+        put_byte(b, (unsigned char)((c->chr >> 16) & 0xFF));
+        put_uint16(b, c->chr & 0xFFFF);
+    } else {
+        put_byte(b, 0xF0);
+        put_uint32(b, c->chr);
+    }
+    *state = c->chr & ~0xFF;
+}
+static void makeliteral_attr(strbuf *b, termchar *c, unsigned long *state)
+{
+    /*
+     * My encoding for attributes is 16-bit-granular and assumes
+     * that the top bit of the word is never required. I either
+     * store a two-byte value with the top bit clear (indicating
+     * just that value), or a four-byte value with the top bit set
+     * (indicating the same value with its top bit clear).
+     *
+     * However, first I permute the bits of the attribute value, so
+     * that the eight bits of colour (four in each of fg and bg)
+     * which are never non-zero unless xterm 256-colour mode is in
+     * use are placed higher up the word than everything else. This
+     * ensures that attribute values remain 16-bit _unless_ the
+     * user uses extended colour.
+     */
+    unsigned attr, colourbits;
+
+    attr = c->attr;
+
+    assert(ATTR_BGSHIFT > ATTR_FGSHIFT);
+
+    colourbits = (attr >> (ATTR_BGSHIFT + 4)) & 0xF;
+    colourbits <<= 4;
+    colourbits |= (attr >> (ATTR_FGSHIFT + 4)) & 0xF;
+
+    attr = (((attr >> (ATTR_BGSHIFT + 8)) << (ATTR_BGSHIFT + 4)) |
+            (attr & ((1 << (ATTR_BGSHIFT + 4))-1)));
+    attr = (((attr >> (ATTR_FGSHIFT + 8)) << (ATTR_FGSHIFT + 4)) |
+            (attr & ((1 << (ATTR_FGSHIFT + 4))-1)));
+
+    attr |= (colourbits << (32-9));
+
+    if (attr < 0x8000) {
+        put_byte(b, (unsigned char)((attr >> 8) & 0xFF));
+        put_byte(b, (unsigned char)(attr & 0xFF));
+    } else {
+        put_byte(b, (unsigned char)(((attr >> 24) & 0x7F) | 0x80));
+        put_byte(b, (unsigned char)((attr >> 16) & 0xFF));
+        put_byte(b, (unsigned char)((attr >> 8) & 0xFF));
+        put_byte(b, (unsigned char)(attr & 0xFF));
+    }
+}
+static void makeliteral_truecolour(strbuf *b, termchar *c, unsigned long *state)
+{
+    /*
+     * Put the used parts of the colour info into the buffer.
+     */
+    put_byte(b, ((c->truecolour.fg.enabled ? 1 : 0) |
+                 (c->truecolour.bg.enabled ? 2 : 0)));
+    if (c->truecolour.fg.enabled) {
+        put_byte(b, c->truecolour.fg.r);
+        put_byte(b, c->truecolour.fg.g);
+        put_byte(b, c->truecolour.fg.b);
+    }
+    if (c->truecolour.bg.enabled) {
+        put_byte(b, c->truecolour.bg.r);
+        put_byte(b, c->truecolour.bg.g);
+        put_byte(b, c->truecolour.bg.b);
+    }
+}
+static void makeliteral_cc(strbuf *b, termchar *c, unsigned long *state)
+{
+    /*
+     * For combining characters, I just encode a bunch of ordinary
+     * chars using makeliteral_chr, and terminate with a \0
+     * character (which I know won't come up as a combining char
+     * itself).
+     *
+     * I don't use the stateful encoding in makeliteral_chr.
+     */
+    unsigned long zstate;
+    termchar z;
+
+    while (c->cc_next) {
+        c += c->cc_next;
+
+        assert(c->chr != 0);
+
+        zstate = 0;
+        makeliteral_chr(b, c, &zstate);
+    }
+
+    z.chr = 0;
+    zstate = 0;
+    makeliteral_chr(b, &z, &zstate);
+}
+
+typedef struct compressed_scrollback_line {
+    size_t len;
+    /* compressed data follows after this */
+} compressed_scrollback_line;
+
+static termline *decompressline_no_free(compressed_scrollback_line *line);
+
+static compressed_scrollback_line *compressline_no_free(termline *ldata)
+{
+    strbuf *b = strbuf_new();
+
+    /* Leave space for the header structure */
+    strbuf_append(b, sizeof(compressed_scrollback_line));
+
+    /*
+     * First, store the column count, 7 bits at a time, least
+     * significant `digit' first, with the high bit set on all but
+     * the last.
+     */
+    {
+        int n = ldata->cols;
+        while (n >= 128) {
+            put_byte(b, (unsigned char)((n & 0x7F) | 0x80));
+            n >>= 7;
+        }
+        put_byte(b, (unsigned char)(n));
+    }
+
+    /*
+     * Next store the lattrs; same principle. We add one extra bit to
+     * this to indicate the trust state of the line.
+     */
+    {
+        int n = ldata->lattr | (ldata->trusted ? 0x10000 : 0);
+        while (n >= 128) {
+            put_byte(b, (unsigned char)((n & 0x7F) | 0x80));
+            n >>= 7;
+        }
+        put_byte(b, (unsigned char)(n));
+    }
+
+    /*
+     * Now we store a sequence of separate run-length encoded
+     * fragments, each containing exactly as many symbols as there
+     * are columns in the ldata.
+     *
+     * All of these have a common basic format:
+     *
+     *  - a byte 00-7F indicates that X+1 literals follow it
+     *  - a byte 80-FF indicates that a single literal follows it
+     *    and expects to be repeated (X-0x80)+2 times.
+     *
+     * The format of the `literals' varies between the fragments.
+     */
+    makerle(b, ldata, makeliteral_chr);
+    makerle(b, ldata, makeliteral_attr);
+    makerle(b, ldata, makeliteral_truecolour);
+    makerle(b, ldata, makeliteral_cc);
+
+    size_t linelen = b->len - sizeof(compressed_scrollback_line);
+    compressed_scrollback_line *line =
+        (compressed_scrollback_line *)strbuf_to_str(b);
+    line->len = linelen;
+
+    /*
+     * Diagnostics: ensure that the compressed data really does
+     * decompress to the right thing.
+     *
+     * This is a bit performance-heavy for production code.
+     */
+#ifdef TERM_CC_DIAGS
+#ifndef CHECK_SB_COMPRESSION
+    {
+        termline *dcl;
+        int i;
+
+#ifdef DIAGNOSTIC_SB_COMPRESSION
+        for (i = 0; i < b->len; i++) {
+            printf(" %02x ", b->data[i]);
+        }
+        printf("\n");
+#endif
+
+        dcl = decompressline_no_free(line);
+        assert(ldata->cols == dcl->cols);
+        assert(ldata->lattr == dcl->lattr);
+        for (i = 0; i < ldata->cols; i++)
+            assert(termchars_equal(&ldata->chars[i], &dcl->chars[i]));
+
+#ifdef DIAGNOSTIC_SB_COMPRESSION
+        printf("%d cols (%d bytes) -> %d bytes (factor of %g)\n",
+               ldata->cols, 4 * ldata->cols, dused,
+               (double)dused / (4 * ldata->cols));
+#endif
+
+        freetermline(dcl);
+    }
+#endif
+#endif /* TERM_CC_DIAGS */
+
+    return line;
+}
+
+static compressed_scrollback_line *compressline_and_free(termline *ldata)
+{
+    compressed_scrollback_line *cline = compressline_no_free(ldata);
+    freetermline(ldata);
+    return cline;
+}
+
+static void readrle(BinarySource *bs, termline *ldata,
+                    void (*readliteral)(BinarySource *bs, termchar *c,
+                                        termline *ldata, unsigned long *state))
+{
+    int n = 0;
+    unsigned long state = 0;
+
+    while (n < ldata->cols) {
+        int hdr = get_byte(bs);
+
+        if (hdr >= 0x80) {
+            /* A run. */
+
+            size_t pos = bs->pos, count = hdr + 2 - 0x80;
+            while (count--) {
+                assert(n < ldata->cols);
+                bs->pos = pos;
+                readliteral(bs, ldata->chars + n, ldata, &state);
+                n++;
+            }
+        } else {
+            /* Just a sequence of consecutive literals. */
+
+            int count = hdr + 1;
+            while (count--) {
+                assert(n < ldata->cols);
+                readliteral(bs, ldata->chars + n, ldata, &state);
+                n++;
+            }
+        }
+    }
+
+    assert(n == ldata->cols);
+}
+static void readliteral_chr(BinarySource *bs, termchar *c, termline *ldata,
+                            unsigned long *state)
+{
+    int byte;
+
+    /*
+     * 00000000-0000007F: 0xxxxxxx
+     * 00000080-00003FFF: 10xxxxxx xxxxxxxx
+     * 00004000-001FFFFF: 110xxxxx xxxxxxxx xxxxxxxx
+     * 00200000-0FFFFFFF: 1110xxxx xxxxxxxx xxxxxxxx xxxxxxxx
+     * 10000000-FFFFFFFF: 11110ZZZ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
+     */
+
+    byte = get_byte(bs);
+    if (byte < 0x80) {
+        c->chr = byte | *state;
+    } else if (byte < 0xC0) {
+        c->chr = (byte &~ 0xC0) << 8;
+        c->chr |= get_byte(bs);
+    } else if (byte < 0xE0) {
+        c->chr = (byte &~ 0xE0) << 16;
+        c->chr |= get_uint16(bs);
+    } else if (byte < 0xF0) {
+        c->chr = (byte &~ 0xF0) << 24;
+        c->chr |= get_byte(bs) << 16;
+        c->chr |= get_uint16(bs);
+    } else {
+        assert(byte == 0xF0);
+        c->chr = get_uint32(bs);
+    }
+    *state = c->chr & ~0xFF;
+}
+static void readliteral_attr(BinarySource *bs, termchar *c, termline *ldata,
+                             unsigned long *state)
+{
+    unsigned val, attr, colourbits;
+
+    val = get_uint16(bs);
+
+    if (val >= 0x8000) {
+        val &= ~0x8000;
+        val <<= 16;
+        val |= get_uint16(bs);
+    }
+
+    colourbits = (val >> (32-9)) & 0xFF;
+    attr = (val & ((1<<(32-9))-1));
+
+    attr = (((attr >> (ATTR_FGSHIFT + 4)) << (ATTR_FGSHIFT + 8)) |
+            (attr & ((1 << (ATTR_FGSHIFT + 4))-1)));
+    attr = (((attr >> (ATTR_BGSHIFT + 4)) << (ATTR_BGSHIFT + 8)) |
+            (attr & ((1 << (ATTR_BGSHIFT + 4))-1)));
+
+    attr |= (colourbits >> 4) << (ATTR_BGSHIFT + 4);
+    attr |= (colourbits & 0xF) << (ATTR_FGSHIFT + 4);
+
+    c->attr = attr;
+}
+static void readliteral_truecolour(
+    BinarySource *bs, termchar *c, termline *ldata, unsigned long *state)
+{
+    int flags = get_byte(bs);
+
+    if (flags & 1) {
+        c->truecolour.fg.enabled = true;
+        c->truecolour.fg.r = get_byte(bs);
+        c->truecolour.fg.g = get_byte(bs);
+        c->truecolour.fg.b = get_byte(bs);
+    } else {
+        c->truecolour.fg = optionalrgb_none;
+    }
+
+    if (flags & 2) {
+        c->truecolour.bg.enabled = true;
+        c->truecolour.bg.r = get_byte(bs);
+        c->truecolour.bg.g = get_byte(bs);
+        c->truecolour.bg.b = get_byte(bs);
+    } else {
+        c->truecolour.bg = optionalrgb_none;
+    }
+}
+static void readliteral_cc(BinarySource *bs, termchar *c, termline *ldata,
+                           unsigned long *state)
+{
+    termchar n;
+    unsigned long zstate;
+    int x = c - ldata->chars;
+
+    c->cc_next = 0;
+
+    while (1) {
+        zstate = 0;
+        readliteral_chr(bs, &n, ldata, &zstate);
+        if (!n.chr)
+            break;
+        add_cc(ldata, x, n.chr);
+    }
+}
+
+static termline *decompressline_no_free(compressed_scrollback_line *line)
+{
+    int ncols, byte, shift;
+    BinarySource bs[1];
+    termline *ldata;
+
+    BinarySource_BARE_INIT(bs, line+1, line->len);
+
+    /*
+     * First read in the column count.
+     */
+    ncols = shift = 0;
+    do {
+        byte = get_byte(bs);
+        ncols |= (byte & 0x7F) << shift;
+        shift += 7;
+    } while (byte & 0x80);
+
+    /*
+     * Now create the output termline.
+     */
+    ldata = snew(termline);
+    ldata->chars = snewn(ncols, termchar);
+    ldata->cols = ldata->size = ncols;
+    ldata->temporary = true;
+    ldata->cc_free = 0;
+
+    /*
+     * We must set all the cc pointers in ldata->chars to 0 right
+     * now, so that cc diagnostics that verify the integrity of the
+     * whole line will make sense while we're in the middle of
+     * building it up.
+     */
+    {
+        int i;
+        for (i = 0; i < ldata->cols; i++)
+            ldata->chars[i].cc_next = 0;
+    }
+
+    /*
+     * Now read in the lattr.
+     */
+    int lattr = shift = 0;
+    do {
+        byte = get_byte(bs);
+        lattr |= (byte & 0x7F) << shift;
+        shift += 7;
+    } while (byte & 0x80);
+    ldata->lattr = lattr & 0xFFFF;
+    ldata->trusted = (lattr & 0x10000) != 0;
+
+    /*
+     * Now we read in each of the RLE streams in turn.
+     */
+    readrle(bs, ldata, readliteral_chr);
+    readrle(bs, ldata, readliteral_attr);
+    readrle(bs, ldata, readliteral_truecolour);
+    readrle(bs, ldata, readliteral_cc);
+
+    /* And we always expect that we ended up exactly at the end of the
+     * compressed data. */
+    assert(!get_err(bs));
+    assert(get_avail(bs) == 0);
+
+    return ldata;
+}
+
+static inline void free_compressed_line(compressed_scrollback_line *cline)
+{
+    sfree(cline);
+}
+
+static termline *decompressline_and_free(compressed_scrollback_line *cline)
+{
+    termline *ldata = decompressline_no_free(cline);
+    free_compressed_line(cline);
+    return ldata;
+}
+
+#else /* NO_SCROLLBACK_COMPRESSION */
+
+static termline *duptermline(termline *oldline)
+{
+    termline *newline = snew(termline);
+    *newline = *oldline;               /* copy the POD structure fields */
+    newline->chars = snewn(newline->size, termchar);
+    for (int j = 0; j < newline->size; j++)
+        newline->chars[j] = oldline->chars[j];
+    return newline;
+}
+
+typedef termline compressed_scrollback_line;
+
+static inline compressed_scrollback_line *compressline_and_free(
+    termline *ldata)
+{
+    return ldata;
+}
+
+static inline compressed_scrollback_line *compressline_no_free(termline *ldata)
+{
+    return duptermline(ldata);
+}
+
+static inline termline *decompressline_no_free(
+    compressed_scrollback_line *line)
+{
+    /* This will return a line without the 'temporary' flag, which
+     * means that unlineptr() is already set up to avoid freeing it */
+    return line;
+}
+
+static inline termline *decompressline_and_free(
+    compressed_scrollback_line *line)
+{
+    /* Same as decompressline_no_free, because the caller will free
+     * our returned termline, and that does all the freeing necessary */
+    return line;
+}
+
+static inline void free_compressed_line(compressed_scrollback_line *line)
+{
+    freetermline(line);
+}
+
+#endif /* NO_SCROLLBACK_COMPRESSION */
+
+/*
+ * Resize a line to make it `cols' columns wide.
+ */
+static void resizeline(Terminal *term, termline *line, int cols)
+{
+    int i, oldcols;
+
+    if (line->cols != cols) {
+
+        oldcols = line->cols;
+
+        /*
+         * This line is the wrong length, which probably means it
+         * hasn't been accessed since a resize. Resize it now.
+         *
+         * First, go through all the characters that will be thrown
+         * out in the resize (if we're shrinking the line) and
+         * return their cc lists to the cc free list.
+         */
+        for (i = cols; i < oldcols; i++)
+            clear_cc(line, i);
+
+        /*
+         * If we're shrinking the line, we now bodily move the
+         * entire cc section from where it started to where it now
+         * needs to be. (We have to do this before the resize, so
+         * that the data we're copying is still there. However, if
+         * we're expanding, we have to wait until _after_ the
+         * resize so that the space we're copying into is there.)
+         */
+        if (cols < oldcols)
+            memmove(line->chars + cols, line->chars + oldcols,
+                    (line->size - line->cols) * TSIZE);
+
+        /*
+         * Now do the actual resize, leaving the _same_ amount of
+         * cc space as there was to begin with.
+         */
+        line->size += cols - oldcols;
+        line->chars = sresize(line->chars, line->size, TTYPE);
+        line->cols = cols;
+
+        /*
+         * If we're expanding the line, _now_ we move the cc
+         * section.
+         */
+        if (cols > oldcols)
+            memmove(line->chars + cols, line->chars + oldcols,
+                    (line->size - line->cols) * TSIZE);
+
+        /*
+         * Go through what's left of the original line, and adjust
+         * the first cc_next pointer in each list. (All the
+         * subsequent ones are still valid because they are
+         * relative offsets within the cc block.) Also do the same
+         * to the head of the cc_free list.
+         */
+        for (i = 0; i < oldcols && i < cols; i++)
+            if (line->chars[i].cc_next)
+                line->chars[i].cc_next += cols - oldcols;
+        if (line->cc_free)
+            line->cc_free += cols - oldcols;
+
+        /*
+         * And finally fill in the new space with erase chars. (We
+         * don't have to worry about cc lists here, because we
+         * _know_ the erase char doesn't have one.)
+         */
+        for (i = oldcols; i < cols; i++)
+            line->chars[i] = term->basic_erase_char;
+
+#ifdef TERM_CC_DIAGS
+        cc_check(line);
+#endif
+    }
+}
+
+/*
+ * Get the number of lines in the scrollback.
+ */
+static int sblines(Terminal *term)
+{
+    int sblines = count234(term->scrollback);
+    if (term->erase_to_scrollback &&
+        term->alt_which && term->alt_screen) {
+        sblines += term->alt_sblines;
+    }
+    return sblines;
+}
+
+static void null_line_error(Terminal *term, int y, int lineno,
+                            tree234 *whichtree, int treeindex,
+                            const char *varname)
+{
+    modalfatalbox("%s==NULL in terminal.c\n"
+                  "lineno=%d y=%d w=%d h=%d\n"
+                  "count(scrollback=%p)=%d\n"
+                  "count(screen=%p)=%d\n"
+                  "count(alt=%p)=%d alt_sblines=%d\n"
+                  "whichtree=%p treeindex=%d\n"
+                  "commitid=%s\n\n"
+                  "Please contact <putty@projects.tartarus.org> "
+                  "and pass on the above information.",
+                  varname, lineno, y, term->cols, term->rows,
+                  term->scrollback, count234(term->scrollback),
+                  term->screen, count234(term->screen),
+                  term->alt_screen, count234(term->alt_screen),
+                  term->alt_sblines, whichtree, treeindex, commitid);
+}
+
+/*
+ * Retrieve a line of the screen or of the scrollback, according to
+ * whether the y coordinate is non-negative or negative
+ * (respectively).
+ */
+static termline *lineptr(Terminal *term, int y, int lineno, int screen)
+{
+    termline *line;
+    tree234 *whichtree;
+    int treeindex;
+
+    if (y >= 0) {
+        whichtree = term->screen;
+        treeindex = y;
+    } else {
+        int altlines = 0;
+
+        assert(!screen);
+
+        if (term->erase_to_scrollback &&
+            term->alt_which && term->alt_screen) {
+            altlines = term->alt_sblines;
+        }
+        if (y < -altlines) {
+            whichtree = term->scrollback;
+            treeindex = y + altlines + count234(term->scrollback);
+        } else {
+            whichtree = term->alt_screen;
+            treeindex = y + term->alt_sblines;
+            /* treeindex = y + count234(term->alt_screen); */
+        }
+    }
+    if (whichtree == term->scrollback) {
+        compressed_scrollback_line *cline = index234(whichtree, treeindex);
+        if (!cline)
+            null_line_error(term, y, lineno, whichtree, treeindex, "cline");
+        line = decompressline_no_free(cline);
+    } else {
+        line = index234(whichtree, treeindex);
+    }
+
+    /* We assume that we don't screw up and retrieve something out of range. */
+    if (line == NULL)
+        null_line_error(term, y, lineno, whichtree, treeindex, "line");
+    assert(line != NULL);
+
+    /*
+     * Here we resize lines to _at least_ the right length, but we
+     * don't truncate them. Truncation is done as a side effect of
+     * modifying the line.
+     *
+     * The point of this policy is to try to arrange that resizing the
+     * terminal window repeatedly - e.g. successive steps in an X11
+     * opaque window-resize drag, or resizing as a side effect of
+     * retiling by tiling WMs such as xmonad - does not throw away
+     * data gratuitously. Specifically, we want a sequence of resize
+     * operations with no terminal output between them to have the
+     * same effect as a single resize to the ultimate terminal size,
+     * and also (for the case in which xmonad narrows a window that's
+     * scrolling things) we want scrolling up new text at the bottom
+     * of a narrowed window to avoid truncating lines further up when
+     * the window is re-widened.
+     */
+    if (term->cols > line->cols)
+        resizeline(term, line, term->cols);
+
+    return line;
+}
+
+#define lineptr(x) (lineptr)(term,x,__LINE__,0)
+#define scrlineptr(x) (lineptr)(term,x,__LINE__,1)
+
+/*
+ * Coerce a termline to the terminal's current width. Unlike the
+ * optional resize in lineptr() above, this is potentially destructive
+ * of text, since it can shrink as well as grow the line.
+ *
+ * We call this whenever a termline is actually going to be modified.
+ * Helpfully, putting a single call to this function in check_boundary
+ * deals with _nearly_ all such cases, leaving only a few things like
+ * bulk erase and ESC#8 to handle separately.
+ */
+static void check_line_size(Terminal *term, termline *line)
+{
+    if (term->cols != line->cols)      /* trivial optimisation */
+        resizeline(term, line, term->cols);
+}
+
+static void term_schedule_tblink(Terminal *term);
+static void term_schedule_cblink(Terminal *term);
+static void term_update_callback(void *ctx);
+
+static void term_timer(void *ctx, unsigned long now)
+{
+    Terminal *term = (Terminal *)ctx;
+
+    if (term->tblink_pending && now == term->next_tblink) {
+        term->tblinker = !term->tblinker;
+        term->tblink_pending = false;
+        term_schedule_tblink(term);
+        term->window_update_pending = true;
+    }
+
+    if (term->cblink_pending && now == term->next_cblink) {
+        term->cblinker = !term->cblinker;
+        term->cblink_pending = false;
+        term_schedule_cblink(term);
+        term->window_update_pending = true;
+    }
+
+    if (term->in_vbell && now == term->vbell_end) {
+        term->in_vbell = false;
+        term->window_update_pending = true;
+    }
+
+    if (term->window_update_cooldown &&
+        now == term->window_update_cooldown_end) {
+        term->window_update_cooldown = false;
+    }
+
+    if (term->window_update_pending)
+        term_update_callback(term);
+}
+
+static void term_update_callback(void *ctx)
+{
+    Terminal *term = (Terminal *)ctx;
+    if (!term->window_update_pending)
+        return;
+    if (!term->window_update_cooldown) {
+        term_update(term);
+        term->window_update_cooldown = true;
+        term->window_update_cooldown_end = schedule_timer(
+            UPDATE_DELAY, term_timer, term);
+    }
+}
+
+static void term_schedule_update(Terminal *term)
+{
+    if (!term->window_update_pending) {
+        term->window_update_pending = true;
+        queue_toplevel_callback(term_update_callback, term);
+    }
+}
+
+/*
+ * Call this whenever the terminal window state changes, to queue an
+ * update. This also resets the phase of cursor blinking, so that the
+ * cursor remains visible as it moves with the output, and sets a flag
+ * to indicate that if we have the 'reset scrollback on display
+ * activity' setting enabled, then we should activate it.
+ */
+static void seen_disp_event(Terminal *term)
+{
+    if (term->scroll_on_disp) {
+        term->disptop = 0;
+        term->win_scrollbar_update_pending = true;
+    }
+    term->cblinker = true;
+    term->cblink_pending = false;
+    term_schedule_cblink(term);
+    term_schedule_update(term);
+}
+
+/*
+ * Call when the terminal's blinking-text settings change, or when
+ * a text blink has just occurred.
+ */
+static void term_schedule_tblink(Terminal *term)
+{
+    if (term->blink_is_real) {
+        if (!term->tblink_pending)
+            term->next_tblink = schedule_timer(TBLINK_DELAY, term_timer, term);
+        term->tblink_pending = true;
+    } else {
+        term->tblinker = true;         /* reset when not in use */
+        term->tblink_pending = false;
+    }
+}
+
+/*
+ * Likewise with cursor blinks.
+ */
+static void term_schedule_cblink(Terminal *term)
+{
+    if (term->blink_cur && term->has_focus) {
+        if (!term->cblink_pending)
+            term->next_cblink = schedule_timer(CBLINK_DELAY, term_timer, term);
+        term->cblink_pending = true;
+    } else {
+        term->cblinker = true;         /* reset when not in use */
+        term->cblink_pending = false;
+    }
+}
+
+/*
+ * Call to begin a visual bell.
+ */
+static void term_schedule_vbell(Terminal *term, bool already_started,
+                                long startpoint)
+{
+    long ticks_already_gone;
+
+    if (already_started)
+        ticks_already_gone = GETTICKCOUNT() - startpoint;
+    else
+        ticks_already_gone = 0;
+
+    if (ticks_already_gone < VBELL_DELAY) {
+        term->in_vbell = true;
+        term->vbell_end = schedule_timer(VBELL_DELAY - ticks_already_gone,
+                                         term_timer, term);
+    } else {
+        term->in_vbell = false;
+    }
+}
+
+/*
+ * Set up power-on settings for the terminal.
+ * If 'clear' is false, don't actually clear the primary screen, and
+ * position the cursor below the last non-blank line (scrolling if
+ * necessary).
+ */
+static void power_on(Terminal *term, bool clear)
+{
+    term->alt_x = term->alt_y = 0;
+    term->savecurs.x = term->savecurs.y = 0;
+    term->alt_savecurs.x = term->alt_savecurs.y = 0;
+    term->alt_t = term->marg_t = 0;
+    if (term->rows != -1)
+        term->alt_b = term->marg_b = term->rows - 1;
+    else
+        term->alt_b = term->marg_b = 0;
+    if (term->cols != -1) {
+        int i;
+        for (i = 0; i < term->cols; i++)
+            term->tabs[i] = (i % 8 == 0 ? true : false);
+    }
+    term->alt_om = term->dec_om = conf_get_bool(term->conf, CONF_dec_om);
+    term->alt_ins = false;
+    term->insert = false;
+    term->alt_wnext = false;
+    term->wrapnext = false;
+    term->save_wnext = false;
+    term->alt_save_wnext = false;
+    term->alt_wrap = term->wrap = conf_get_bool(term->conf, CONF_wrap_mode);
+    term->alt_cset = term->cset = term->save_cset = term->alt_save_cset = 0;
+    term->alt_utf = false;
+    term->utf = false;
+    term->save_utf = false;
+    term->alt_save_utf = false;
+    term->utf8.state = 0;
+    term->alt_sco_acs = term->sco_acs =
+        term->save_sco_acs = term->alt_save_sco_acs = 0;
+    term->cset_attr[0] = term->cset_attr[1] =
+        term->save_csattr = term->alt_save_csattr = CSET_ASCII;
+    term->rvideo = false;
+    term->in_vbell = false;
+    term->cursor_on = true;
+    term->big_cursor = false;
+    term->default_attr = term->save_attr =
+        term->alt_save_attr = term->curr_attr = ATTR_DEFAULT;
+    term->curr_truecolour.fg = term->curr_truecolour.bg = optionalrgb_none;
+    term->save_truecolour = term->alt_save_truecolour = term->curr_truecolour;
+    term->app_cursor_keys = conf_get_bool(term->conf, CONF_app_cursor);
+    term->app_keypad_keys = conf_get_bool(term->conf, CONF_app_keypad);
+    term->use_bce = conf_get_bool(term->conf, CONF_bce);
+    term->blink_is_real = conf_get_bool(term->conf, CONF_blinktext);
+    term->erase_char = term->basic_erase_char;
+    term->alt_which = 0;
+    term_print_finish(term);
+    term->xterm_mouse = 0;
+    term->xterm_extended_mouse = false;
+    term->urxvt_extended_mouse = false;
+    term->raw_mouse_reported_x = 0;
+    term->raw_mouse_reported_y = 0;
+    win_set_raw_mouse_mode(term->win, false);
+    term->win_pointer_shape_pending = true;
+    term->win_pointer_shape_raw = false;
+    term->bracketed_paste = false;
+    term->srm_echo = false;
+    {
+        int i;
+        for (i = 0; i < 256; i++)
+            term->wordness[i] = conf_get_int_int(term->conf, CONF_wordness, i);
+    }
+    if (term->screen) {
+        swap_screen(term, 1, false, false);
+        erase_lots(term, false, true, true);
+        swap_screen(term, 0, false, false);
+        if (clear)
+            erase_lots(term, false, true, true);
+        term->curs.y = find_last_nonempty_line(term, term->screen) + 1;
+        if (term->curs.y == term->rows) {
+            term->curs.y--;
+            scroll(term, 0, term->rows - 1, 1, true);
+        }
+    } else {
+        term->curs.y = 0;
+    }
+    term->curs.x = 0;
+    term_schedule_tblink(term);
+    term_schedule_cblink(term);
+    term_schedule_update(term);
+}
+
+/*
+ * Force a screen update.
+ */
+void term_update(Terminal *term)
+{
+    term->window_update_pending = false;
+
+    if (term->win_move_pending) {
+        win_move(term->win, term->win_move_pending_x,
+                 term->win_move_pending_y);
+        term->win_move_pending = false;
+    }
+    if (term->win_resize_pending == WIN_RESIZE_NEED_SEND) {
+        term->win_resize_pending = WIN_RESIZE_AWAIT_REPLY;
+        win_request_resize(term->win, term->win_resize_pending_w,
+                           term->win_resize_pending_h);
+    }
+    if (term->win_zorder_pending) {
+        win_set_zorder(term->win, term->win_zorder_top);
+        term->win_zorder_pending = false;
+    }
+    if (term->win_minimise_pending) {
+        win_set_minimised(term->win, term->win_minimise_enable);
+        term->win_minimise_pending = false;
+    }
+    if (term->win_maximise_pending) {
+        win_set_maximised(term->win, term->win_maximise_enable);
+        term->win_maximise_pending = false;
+    }
+    if (term->win_title_pending) {
+        win_set_title(term->win, term->window_title,
+                      term->wintitle_codepage);
+        term->win_title_pending = false;
+    }
+    if (term->win_icon_title_pending) {
+        win_set_icon_title(term->win, term->icon_title,
+                           term->icontitle_codepage);
+        term->win_icon_title_pending = false;
+    }
+    if (term->win_pointer_shape_pending) {
+        win_set_raw_mouse_mode_pointer(term->win, term->win_pointer_shape_raw);
+        term->win_pointer_shape_pending = false;
+    }
+    if (term->win_refresh_pending) {
+        win_refresh(term->win);
+        term->win_refresh_pending = false;
+    }
+    if (term->win_palette_pending) {
+        unsigned start = term->win_palette_pending_min;
+        unsigned ncolours = term->win_palette_pending_limit - start;
+        win_palette_set(term->win, start, ncolours, term->palette + start);
+        term->win_palette_pending = false;
+    }
+
+    if (win_setup_draw_ctx(term->win)) {
+        if (term->win_scrollbar_update_pending) {
+            term->win_scrollbar_update_pending = false;
+            update_sbar(term);
+        }
+        do_paint(term);
+        win_set_cursor_pos(
+            term->win, term->curs.x, term->curs.y - term->disptop);
+        win_free_draw_ctx(term->win);
+    }
+}
+
+/*
+ * Called from front end when a keypress occurs, to trigger
+ * anything magical that needs to happen in that situation.
+ */
+void term_seen_key_event(Terminal *term)
+{
+    /*
+     * On any keypress, clear the bell overload mechanism
+     * completely, on the grounds that large numbers of
+     * beeps coming from deliberate key action are likely
+     * to be intended (e.g. beeps from filename completion
+     * blocking repeatedly).
+     */
+    term->beep_overloaded = false;
+    while (term->beephead) {
+        struct beeptime *tmp = term->beephead;
+        term->beephead = tmp->next;
+        sfree(tmp);
+    }
+    term->beeptail = NULL;
+    term->nbeeps = 0;
+
+    /*
+     * Reset the scrollback on keypress, if we're doing that.
+     */
+    if (term->scroll_on_key && term->disptop != 0) {
+        term->disptop = 0;
+        term->win_scrollbar_update_pending = true;
+        term_schedule_update(term);
+    }
+}
+
+/*
+ * Same as power_on(), but an external function.
+ */
+void term_pwron(Terminal *term, bool clear)
+{
+    power_on(term, clear);
+    if (term->ldisc)                   /* cause ldisc to notice changes */
+        ldisc_echoedit_update(term->ldisc);
+    term->disptop = 0;
+    deselect(term);
+    term_update(term);
+}
+
+static void set_erase_char(Terminal *term)
+{
+    term->erase_char = term->basic_erase_char;
+    if (term->use_bce) {
+        term->erase_char.attr = (term->curr_attr &
+                                 (ATTR_FGMASK | ATTR_BGMASK));
+        term->erase_char.truecolour.bg = term->curr_truecolour.bg;
+    }
+}
+
+/*
+ * We copy a bunch of stuff out of the Conf structure into local
+ * fields in the Terminal structure, to avoid the repeated tree234
+ * lookups which would be involved in fetching them from the former
+ * every time.
+ */
+static void term_copy_stuff_from_conf(Terminal *term)
+{
+    term->ansi_colour = conf_get_bool(term->conf, CONF_ansi_colour);
+    term->no_arabicshaping = conf_get_bool(term->conf, CONF_no_arabicshaping);
+    term->beep = conf_get_int(term->conf, CONF_beep);
+    term->bellovl = conf_get_bool(term->conf, CONF_bellovl);
+    term->bellovl_n = conf_get_int(term->conf, CONF_bellovl_n);
+    term->bellovl_s = conf_get_int(term->conf, CONF_bellovl_s);
+    term->bellovl_t = conf_get_int(term->conf, CONF_bellovl_t);
+    term->no_bidi = conf_get_bool(term->conf, CONF_no_bidi);
+    term->bksp_is_delete = conf_get_bool(term->conf, CONF_bksp_is_delete);
+    term->blink_cur = conf_get_bool(term->conf, CONF_blink_cur);
+    term->blinktext = conf_get_bool(term->conf, CONF_blinktext);
+    term->cjk_ambig_wide = conf_get_bool(term->conf, CONF_cjk_ambig_wide);
+    term->conf_height = conf_get_int(term->conf, CONF_height);
+    term->conf_width = conf_get_int(term->conf, CONF_width);
+    term->crhaslf = conf_get_bool(term->conf, CONF_crhaslf);
+    term->erase_to_scrollback = conf_get_bool(term->conf, CONF_erase_to_scrollback);
+    term->funky_type = conf_get_int(term->conf, CONF_funky_type);
+    term->sharrow_type = conf_get_int(term->conf, CONF_sharrow_type);
+    term->lfhascr = conf_get_bool(term->conf, CONF_lfhascr);
+    term->logflush = conf_get_bool(term->conf, CONF_logflush);
+    term->logtype = conf_get_int(term->conf, CONF_logtype);
+    term->mouse_override = conf_get_bool(term->conf, CONF_mouse_override);
+    term->nethack_keypad = conf_get_bool(term->conf, CONF_nethack_keypad);
+    term->no_alt_screen = conf_get_bool(term->conf, CONF_no_alt_screen);
+    term->no_applic_c = conf_get_bool(term->conf, CONF_no_applic_c);
+    term->no_applic_k = conf_get_bool(term->conf, CONF_no_applic_k);
+    term->no_dbackspace = conf_get_bool(term->conf, CONF_no_dbackspace);
+    term->no_mouse_rep = conf_get_bool(term->conf, CONF_no_mouse_rep);
+    term->no_remote_charset = conf_get_bool(term->conf, CONF_no_remote_charset);
+    term->no_remote_resize = conf_get_bool(term->conf, CONF_no_remote_resize);
+    term->no_remote_wintitle = conf_get_bool(term->conf, CONF_no_remote_wintitle);
+    term->no_remote_clearscroll = conf_get_bool(term->conf, CONF_no_remote_clearscroll);
+    term->rawcnp = conf_get_bool(term->conf, CONF_rawcnp);
+    term->utf8linedraw = conf_get_bool(term->conf, CONF_utf8linedraw);
+    term->rect_select = conf_get_bool(term->conf, CONF_rect_select);
+    term->remote_qtitle_action = conf_get_int(term->conf, CONF_remote_qtitle_action);
+    term->rxvt_homeend = conf_get_bool(term->conf, CONF_rxvt_homeend);
+    term->scroll_on_disp = conf_get_bool(term->conf, CONF_scroll_on_disp);
+    term->scroll_on_key = conf_get_bool(term->conf, CONF_scroll_on_key);
+    term->xterm_mouse_forbidden = conf_get_bool(term->conf, CONF_no_mouse_rep);
+    term->xterm_256_colour = conf_get_bool(term->conf, CONF_xterm_256_colour);
+    term->true_colour = conf_get_bool(term->conf, CONF_true_colour);
+
+    /*
+     * Parse the control-character escapes in the configured
+     * answerback string.
+     */
+    {
+        char *answerback = conf_get_str(term->conf, CONF_answerback);
+        int maxlen = strlen(answerback);
+
+        term->answerback = snewn(maxlen, char);
+        term->answerbacklen = 0;
+
+        while (*answerback) {
+            char *n;
+            char c = ctrlparse(answerback, &n);
+            if (n) {
+                term->answerback[term->answerbacklen++] = c;
+                answerback = n;
+            } else {
+                term->answerback[term->answerbacklen++] = *answerback++;
+            }
+        }
+    }
+}
+
+void term_pre_reconfig(Terminal *term, Conf *conf)
+{
+
+    /*
+     * Copy the current window title into the stored previous
+     * configuration, so that doing nothing to the window title field
+     * in the config box doesn't reset the title to its startup state.
+     */
+    conf_set_str(conf, CONF_wintitle, term->window_title);
+}
+
+/*
+ * When the user reconfigures us, we need to check the forbidden-
+ * alternate-screen config option, disable raw mouse mode if the
+ * user has disabled mouse reporting, and abandon a print job if
+ * the user has disabled printing.
+ */
+void term_reconfig(Terminal *term, Conf *conf)
+{
+    /*
+     * Before adopting the new config, check all those terminal
+     * settings which control power-on defaults; and if they've
+     * changed, we will modify the current state as well as the
+     * default one. The full list is: Auto wrap mode, DEC Origin
+     * Mode, BCE, blinking text, character classes.
+     */
+    bool reset_wrap, reset_decom, reset_bce, reset_tblink, reset_charclass;
+    bool palette_changed = false;
+    int i;
+
+    reset_wrap = (conf_get_bool(term->conf, CONF_wrap_mode) !=
+                  conf_get_bool(conf, CONF_wrap_mode));
+    reset_decom = (conf_get_bool(term->conf, CONF_dec_om) !=
+                   conf_get_bool(conf, CONF_dec_om));
+    reset_bce = (conf_get_bool(term->conf, CONF_bce) !=
+                 conf_get_bool(conf, CONF_bce));
+    reset_tblink = (conf_get_bool(term->conf, CONF_blinktext) !=
+                    conf_get_bool(conf, CONF_blinktext));
+    reset_charclass = false;
+    for (i = 0; i < 256; i++)
+        if (conf_get_int_int(term->conf, CONF_wordness, i) !=
+            conf_get_int_int(conf, CONF_wordness, i))
+            reset_charclass = true;
+
+    /*
+     * If the bidi or shaping settings have changed, flush the bidi
+     * cache completely.
+     */
+    if (conf_get_bool(term->conf, CONF_no_arabicshaping) !=
+        conf_get_bool(conf, CONF_no_arabicshaping) ||
+        conf_get_bool(term->conf, CONF_no_bidi) !=
+        conf_get_bool(conf, CONF_no_bidi)) {
+        for (i = 0; i < term->bidi_cache_size; i++) {
+            sfree(term->pre_bidi_cache[i].chars);
+            sfree(term->post_bidi_cache[i].chars);
+            term->pre_bidi_cache[i].width = -1;
+            term->pre_bidi_cache[i].chars = NULL;
+            term->post_bidi_cache[i].width = -1;
+            term->post_bidi_cache[i].chars = NULL;
+        }
+    }
+
+    {
+        const char *old_title = conf_get_str(term->conf, CONF_wintitle);
+        const char *new_title = conf_get_str(conf, CONF_wintitle);
+        if (strcmp(old_title, new_title)) {
+            sfree(term->window_title);
+            term->window_title = dupstr(new_title);
+            term->wintitle_codepage = DEFAULT_CODEPAGE;
+            term->win_title_pending = true;
+            term_schedule_update(term);
+        }
+    }
+
+    /*
+     * Just setting conf is sufficient to cause colour setting changes
+     * to appear on the next ESC]R palette reset. But we should also
+     * check whether any colour settings have been changed, so that
+     * they can be updated immediately if they haven't been overridden
+     * by some escape sequence.
+     */
+    {
+        int i, j;
+        for (i = 0; i < CONF_NCOLOURS; i++) {
+            for (j = 0; j < 3; j++)
+                if (conf_get_int_int(term->conf, CONF_colours, i*3+j) !=
+                    conf_get_int_int(conf, CONF_colours, i*3+j))
+                    break;
+            if (j < 3) {
+                /* Actually enacting the change has to be deferred 
+                 * until the new conf is installed. */
+                palette_changed = true;
+                break;
+            }
+        }
+    }
+
+    conf_free(term->conf);
+    term->conf = conf_copy(conf);
+
+    if (reset_wrap)
+        term->alt_wrap = term->wrap = conf_get_bool(term->conf, CONF_wrap_mode);
+    if (reset_decom)
+        term->alt_om = term->dec_om = conf_get_bool(term->conf, CONF_dec_om);
+    if (reset_bce) {
+        term->use_bce = conf_get_bool(term->conf, CONF_bce);
+        set_erase_char(term);
+    }
+    if (reset_tblink) {
+        term->blink_is_real = conf_get_bool(term->conf, CONF_blinktext);
+    }
+    if (reset_charclass)
+        for (i = 0; i < 256; i++)
+            term->wordness[i] = conf_get_int_int(term->conf, CONF_wordness, i);
+
+    if (conf_get_bool(term->conf, CONF_no_alt_screen))
+        swap_screen(term, 0, false, false);
+    if (conf_get_bool(term->conf, CONF_no_remote_charset)) {
+        term->cset_attr[0] = term->cset_attr[1] = CSET_ASCII;
+        term->sco_acs = term->alt_sco_acs = 0;
+        term->utf = false;
+    }
+    if (!conf_get_str(term->conf, CONF_printer)) {
+        term_print_finish(term);
+    }
+    if (palette_changed)
+        term_notify_palette_changed(term);
+    term_schedule_tblink(term);
+    term_schedule_cblink(term);
+    term_copy_stuff_from_conf(term);
+    term_update_raw_mouse_mode(term);
+}
+
+/*
+ * Clear the scrollback.
+ */
+void term_clrsb(Terminal *term)
+{
+    unsigned char *line;
+    int i;
+
+    /*
+     * Scroll forward to the current screen, if we were back in the
+     * scrollback somewhere until now.
+     */
+    term->disptop = 0;
+
+    /*
+     * Clear the actual scrollback.
+     */
+    while ((line = delpos234(term->scrollback, 0)) != NULL) {
+        sfree(line);            /* this is compressed data, not a termline */
+    }
+
+    /*
+     * When clearing the scrollback, we also truncate any termlines on
+     * the current screen which have remembered data from a previous
+     * larger window size. Rationale: clearing the scrollback is
+     * sometimes done to protect privacy, so the user intention is
+     * specifically that we should not retain evidence of what
+     * previously happened in the terminal, and that ought to include
+     * evidence to the right as well as evidence above.
+     */
+    for (i = 0; i < term->rows; i++)
+        check_line_size(term, scrlineptr(i));
+
+    /*
+     * That operation has invalidated the selection, if it overlapped
+     * the scrollback at all.
+     */
+    if (term->selstate != NO_SELECTION && term->selstart.y < 0)
+        deselect(term);
+
+    /*
+     * There are now no lines of real scrollback which can be pulled
+     * back into the screen by a resize, and no lines of the alternate
+     * screen which should be displayed as if part of the scrollback.
+     */
+    term->tempsblines = 0;
+    term->alt_sblines = 0;
+
+    /*
+     * The scrollbar will need updating to reflect the new state of
+     * the world.
+     */
+    term->win_scrollbar_update_pending = true;
+    term_schedule_update(term);
+}
+
+const optionalrgb optionalrgb_none = {0, 0, 0, 0};
+
+void term_setup_window_titles(Terminal *term, const char *title_hostname)
+{
+    const char *conf_title = conf_get_str(term->conf, CONF_wintitle);
+    sfree(term->window_title);
+    sfree(term->icon_title);
+    if (*conf_title) {
+        term->window_title = dupstr(conf_title);
+        term->icon_title = dupstr(conf_title);
+    } else {
+        if (title_hostname && *title_hostname)
+            term->window_title = dupcat(title_hostname, " - ", appname);
+        else
+            term->window_title = dupstr(appname);
+        term->icon_title = dupstr(term->window_title);
+    }
+    term->wintitle_codepage = term->icontitle_codepage = DEFAULT_CODEPAGE;
+    term->win_title_pending = true;
+    term->win_icon_title_pending = true;
+}
+
+static void palette_rebuild(Terminal *term)
+{
+    unsigned min_changed = OSC4_NCOLOURS, max_changed = 0;
+
+    if (term->win_palette_pending) {
+        /* Possibly extend existing range. */
+        min_changed = term->win_palette_pending_min;
+        max_changed = term->win_palette_pending_limit - 1;
+    } else {
+        /* Start with empty range. */
+        min_changed = OSC4_NCOLOURS;
+        max_changed = 0;
+    }
+
+    for (unsigned i = 0; i < OSC4_NCOLOURS; i++) {
+        rgb new_value;
+        bool found = false;
+
+        for (unsigned j = lenof(term->subpalettes); j-- > 0 ;) {
+            if (term->subpalettes[j].present[i]) {
+                new_value = term->subpalettes[j].values[i];
+                found = true;
+                break;
+            }
+        }
+
+        assert(found);    /* we expect SUBPAL_CONF to always be set */
+
+        if (new_value.r != term->palette[i].r ||
+            new_value.g != term->palette[i].g ||
+            new_value.b != term->palette[i].b) {
+            term->palette[i] = new_value;
+            if (min_changed > i)
+                min_changed = i;
+            if (max_changed < i)
+                max_changed = i;
+        }
+    }
+
+    if (min_changed <= max_changed) {
+        /*
+         * At least one colour changed (or we had an update scheduled
+         * already). Schedule a redraw event to pass the result back
+         * to the TermWin. This also requires invalidating the rest
+         * of the window, because usually all the text will need
+         * redrawing in the new colours.
+         * (If there was an update pending and this palette rebuild
+         * didn't actually change anything, we'll harmlessly reinforce
+         * the existing update request.)
+         */
+        term->win_palette_pending = true;
+        term->win_palette_pending_min = min_changed;
+        term->win_palette_pending_limit = max_changed + 1;
+        term_invalidate(term);
+    }
+}
+
+/*
+ * Rebuild the palette from configuration and platform colours.
+ * If 'keep_overrides' set, any escape-sequence-specified overrides will
+ * remain in place.
+ */
+static void palette_reset(Terminal *term, bool keep_overrides)
+{
+    for (unsigned i = 0; i < OSC4_NCOLOURS; i++)
+        term->subpalettes[SUBPAL_CONF].present[i] = true;
+
+    /*
+     * Copy all the palette information out of the Conf.
+     */
+    for (unsigned i = 0; i < CONF_NCOLOURS; i++) {
+        rgb *col = &term->subpalettes[SUBPAL_CONF].values[
+            colour_indices_conf_to_osc4[i]];
+        col->r = conf_get_int_int(term->conf, CONF_colours, i*3+0);
+        col->g = conf_get_int_int(term->conf, CONF_colours, i*3+1);
+        col->b = conf_get_int_int(term->conf, CONF_colours, i*3+2);
+    }
+
+    /*
+     * Directly invent the rest of the xterm-256 colours.
+     */
+    for (unsigned i = 0; i < 216; i++) {
+        rgb *col = &term->subpalettes[SUBPAL_CONF].values[i + 16];
+        int r = i / 36, g = (i / 6) % 6, b = i % 6;
+        col->r = r ? r * 40 + 55 : 0;
+        col->g = g ? g * 40 + 55 : 0;
+        col->b = b ? b * 40 + 55 : 0;
+    }
+    for (unsigned i = 0; i < 24; i++) {
+        rgb *col = &term->subpalettes[SUBPAL_CONF].values[i + 232];
+        int shade = i * 10 + 8;
+        col->r = col->g = col->b = shade;
+    }
+
+    /*
+     * Re-fetch any OS-local overrides.
+     */
+    for (unsigned i = 0; i < OSC4_NCOLOURS; i++)
+        term->subpalettes[SUBPAL_PLATFORM].present[i] = false;
+    win_palette_get_overrides(term->win, term);
+
+    if (!keep_overrides) {
+        /*
+         * Get rid of all escape-sequence configuration.
+         */
+        for (unsigned i = 0; i < OSC4_NCOLOURS; i++)
+            term->subpalettes[SUBPAL_SESSION].present[i] = false;
+    }
+
+    /*
+     * Rebuild the composite palette.
+     */
+    palette_rebuild(term);
+}
+
+void term_palette_override(Terminal *term, unsigned osc4_index, rgb rgb)
+{
+    /*
+     * We never expect to be called except as re-entry from our own
+     * call to win_palette_get_overrides above, so we need not mess
+     * about calling palette_rebuild.
+     */
+    term->subpalettes[SUBPAL_PLATFORM].present[osc4_index] = true;
+    term->subpalettes[SUBPAL_PLATFORM].values[osc4_index] = rgb;
+}
+
+/*
+ * Initialise the terminal.
+ */
+Terminal *term_init(Conf *myconf, struct unicode_data *ucsdata, TermWin *win)
+{
+    Terminal *term;
+
+    /*
+     * Allocate a new Terminal structure and initialise the fields
+     * that need it.
+     */
+    term = snew(Terminal);
+    term->win = win;
+    term->ucsdata = ucsdata;
+    term->conf = conf_copy(myconf);
+    term->logctx = NULL;
+    term->compatibility_level = TM_PUTTY;
+    strcpy(term->id_string, "\033[?6c");
+    term->cblink_pending = term->tblink_pending = false;
+    term->paste_buffer = NULL;
+    term->paste_len = 0;
+    bufchain_init(&term->inbuf);
+    bufchain_init(&term->printer_buf);
+    term->printing = term->only_printing = false;
+    term->print_job = NULL;
+    term->vt52_mode = false;
+    term->cr_lf_return = false;
+    term->mouse_is_down = 0;
+    term->reset_132 = false;
+    term->cblinker = false;
+    term->tblinker = false;
+    term->has_focus = true;
+    term->repeat_off = false;
+    term->termstate = TOPLEVEL;
+    term->selstate = NO_SELECTION;
+    term->curstype = 0;
+
+    term_copy_stuff_from_conf(term);
+
+    term->screen = term->alt_screen = term->scrollback = NULL;
+    term->tempsblines = 0;
+    term->alt_sblines = 0;
+    term->disptop = 0;
+    term->disptext = NULL;
+    term->dispcursx = term->dispcursy = -1;
+    term->tabs = NULL;
+    deselect(term);
+    term->rows = term->cols = -1;
+    power_on(term, true);
+    term->beephead = term->beeptail = NULL;
+    term->nbeeps = 0;
+    term->lastbeep = false;
+    term->beep_overloaded = false;
+    term->attr_mask = 0xffffffff;
+    term->backend = NULL;
+    term->in_term_out = false;
+    term->ltemp = NULL;
+    term->ltemp_size = 0;
+    term->wcFrom = NULL;
+    term->wcTo = NULL;
+    term->wcFromTo_size = 0;
+
+    term->window_update_pending = false;
+    term->window_update_cooldown = false;
+
+    term->bidi_cache_size = 0;
+    term->pre_bidi_cache = term->post_bidi_cache = NULL;
+
+    /* FULL-TERMCHAR */
+    term->basic_erase_char.chr = CSET_ASCII | ' ';
+    term->basic_erase_char.attr = ATTR_DEFAULT;
+    term->basic_erase_char.cc_next = 0;
+    term->basic_erase_char.truecolour.fg = optionalrgb_none;
+    term->basic_erase_char.truecolour.bg = optionalrgb_none;
+    term->erase_char = term->basic_erase_char;
+
+    term->last_selected_text = NULL;
+    term->last_selected_attr = NULL;
+    term->last_selected_tc = NULL;
+    term->last_selected_len = 0;
+    /* TermWin implementations will typically extend these with
+     * clipboard ids they know about */
+    term->mouse_select_clipboards[0] = CLIP_LOCAL;
+    term->n_mouse_select_clipboards = 1;
+    term->mouse_paste_clipboard = CLIP_NULL;
+
+    term->last_graphic_char = 0;
+
+    term->trusted = true;
+
+    term->bracketed_paste_active = false;
+
+    term->window_title = dupstr("");
+    term->icon_title = dupstr("");
+    term->wintitle_codepage = term->icontitle_codepage = DEFAULT_CODEPAGE;
+    term->minimised = false;
+    term->winpos_x = term->winpos_y = 0;
+    term->winpixsize_x = term->winpixsize_y = 0;
+
+    term->win_move_pending = false;
+    term->win_resize_pending = WIN_RESIZE_NO;
+    term->win_zorder_pending = false;
+    term->win_minimise_pending = false;
+    term->win_maximise_pending = false;
+    term->win_title_pending = false;
+    term->win_icon_title_pending = false;
+    term->win_pointer_shape_pending = false;
+    term->win_refresh_pending = false;
+    term->win_scrollbar_update_pending = false;
+    term->win_palette_pending = false;
+
+    term->bidi_ctx = bidi_new_context();
+
+    palette_reset(term, false);
+
+    return term;
+}
+
+void term_free(Terminal *term)
+{
+    compressed_scrollback_line *cline;
+    termline *line;
+    struct beeptime *beep;
+    int i;
+
+    while ((cline = delpos234(term->scrollback, 0)) != NULL)
+        free_compressed_line(cline);
+    freetree234(term->scrollback);
+    while ((line = delpos234(term->screen, 0)) != NULL)
+        freetermline(line);
+    freetree234(term->screen);
+    while ((line = delpos234(term->alt_screen, 0)) != NULL)
+        freetermline(line);
+    freetree234(term->alt_screen);
+    if (term->disptext) {
+        for (i = 0; i < term->rows; i++)
+            freetermline(term->disptext[i]);
+    }
+    sfree(term->disptext);
+    while (term->beephead) {
+        beep = term->beephead;
+        term->beephead = beep->next;
+        sfree(beep);
+    }
+    bufchain_clear(&term->inbuf);
+    if(term->print_job)
+        printer_finish_job(term->print_job);
+    bufchain_clear(&term->printer_buf);
+    sfree(term->paste_buffer);
+    sfree(term->ltemp);
+    sfree(term->wcFrom);
+    sfree(term->wcTo);
+    sfree(term->answerback);
+
+    for (i = 0; i < term->bidi_cache_size; i++) {
+        sfree(term->pre_bidi_cache[i].chars);
+        sfree(term->post_bidi_cache[i].chars);
+        sfree(term->post_bidi_cache[i].forward);
+        sfree(term->post_bidi_cache[i].backward);
+    }
+    sfree(term->pre_bidi_cache);
+    sfree(term->post_bidi_cache);
+
+    sfree(term->tabs);
+
+    expire_timer_context(term);
+    delete_callbacks_for_context(term);
+
+    conf_free(term->conf);
+
+    sfree(term->window_title);
+    sfree(term->icon_title);
+
+    bidi_free_context(term->bidi_ctx);
+
+    sfree(term);
+}
+
+void term_set_trust_status(Terminal *term, bool trusted)
+{
+    term->trusted = trusted;
+}
+
+void term_get_cursor_position(Terminal *term, int *x, int *y)
+{
+    *x = term->curs.x;
+    *y = term->curs.y;
+}
+
+/*
+ * Set up the terminal for a given size.
+ */
+void term_size(Terminal *term, int newrows, int newcols, int newsavelines)
+{
+    tree234 *newalt;
+    termline **newdisp, *line;
+    int i, j, oldrows = term->rows;
+    int sblen;
+    int save_alt_which = term->alt_which;
+
+    if (newrows == term->rows && newcols == term->cols &&
+        newsavelines == term->savelines)
+        return;                        /* nothing to do */
+
+    /* Behave sensibly if we're given zero (or negative) rows/cols */
+
+    if (newrows < 1) newrows = 1;
+    if (newcols < 1) newcols = 1;
+
+    deselect(term);
+    swap_screen(term, 0, false, false);
+
+    term->alt_t = term->marg_t = 0;
+    term->alt_b = term->marg_b = newrows - 1;
+
+    if (term->rows == -1) {
+        term->scrollback = newtree234(NULL);
+        term->screen = newtree234(NULL);
+        term->tempsblines = 0;
+        term->rows = 0;
+    }
+
+    /*
+     * Resize the screen and scrollback. We only need to shift
+     * lines around within our data structures, because lineptr()
+     * will take care of resizing each individual line if
+     * necessary. So:
+     *
+     *  - If the new screen is longer, we shunt lines in from temporary
+     *    scrollback if possible, otherwise we add new blank lines at
+     *    the bottom.
+     *
+     *  - If the new screen is shorter, we remove any blank lines at
+     *    the bottom if possible, otherwise shunt lines above the cursor
+     *    to scrollback if possible, otherwise delete lines below the
+     *    cursor.
+     *
+     *  - Then, if the new scrollback length is less than the
+     *    amount of scrollback we actually have, we must throw some
+     *    away.
+     */
+    sblen = count234(term->scrollback);
+    /* Do this loop to expand the screen if newrows > rows */
+    assert(term->rows == count234(term->screen));
+    while (term->rows < newrows) {
+        if (term->tempsblines > 0) {
+            compressed_scrollback_line *cline;
+            /* Insert a line from the scrollback at the top of the screen. */
+            assert(sblen >= term->tempsblines);
+            cline = delpos234(term->scrollback, --sblen);
+            line = decompressline_and_free(cline);
+            line->temporary = false;   /* reconstituted line is now real */
+            term->tempsblines -= 1;
+            addpos234(term->screen, line, 0);
+            term->curs.y += 1;
+            term->savecurs.y += 1;
+            term->alt_y += 1;
+            term->alt_savecurs.y += 1;
+        } else {
+            /* Add a new blank line at the bottom of the screen. */
+            line = newtermline(term, newcols, false);
+            addpos234(term->screen, line, count234(term->screen));
+        }
+        term->rows += 1;
+    }
+    /* Do this loop to shrink the screen if newrows < rows */
+    while (term->rows > newrows) {
+        if (term->curs.y < term->rows - 1) {
+            /* delete bottom row, unless it contains the cursor */
+            line = delpos234(term->screen, term->rows - 1);
+            freetermline(line);
+        } else {
+            /* push top row to scrollback */
+            line = delpos234(term->screen, 0);
+            addpos234(term->scrollback, compressline_and_free(line), sblen++);
+            term->tempsblines += 1;
+            term->curs.y -= 1;
+            term->savecurs.y -= 1;
+            term->alt_y -= 1;
+            term->alt_savecurs.y -= 1;
+        }
+        term->rows -= 1;
+    }
+    assert(term->rows == newrows);
+    assert(count234(term->screen) == newrows);
+
+    /* Delete any excess lines from the scrollback. */
+    while (sblen > newsavelines) {
+        line = delpos234(term->scrollback, 0);
+        sfree(line);
+        sblen--;
+    }
+    if (sblen < term->tempsblines)
+        term->tempsblines = sblen;
+    assert(count234(term->scrollback) <= newsavelines);
+    assert(count234(term->scrollback) >= term->tempsblines);
+    term->disptop = 0;
+
+    /* Make a new displayed text buffer. */
+    newdisp = snewn(newrows, termline *);
+    for (i = 0; i < newrows; i++) {
+        newdisp[i] = newtermline(term, newcols, false);
+        for (j = 0; j < newcols; j++)
+            newdisp[i]->chars[j].attr = ATTR_INVALID;
+    }
+    if (term->disptext) {
+        for (i = 0; i < oldrows; i++)
+            freetermline(term->disptext[i]);
+    }
+    sfree(term->disptext);
+    term->disptext = newdisp;
+    term->dispcursx = term->dispcursy = -1;
+
+    /* Make a new alternate screen. */
+    newalt = newtree234(NULL);
+    for (i = 0; i < newrows; i++) {
+        line = newtermline(term, newcols, true);
+        addpos234(newalt, line, i);
+    }
+    if (term->alt_screen) {
+        while (NULL != (line = delpos234(term->alt_screen, 0)))
+            freetermline(line);
+        freetree234(term->alt_screen);
+    }
+    term->alt_screen = newalt;
+    term->alt_sblines = 0;
+
+    term->tabs = sresize(term->tabs, newcols, unsigned char);
+    {
+        int i;
+        for (i = (term->cols > 0 ? term->cols : 0); i < newcols; i++)
+            term->tabs[i] = (i % 8 == 0 ? true : false);
+    }
+
+    /* Check that the cursor positions are still valid. */
+    if (term->savecurs.y < 0)
+        term->savecurs.y = 0;
+    if (term->savecurs.y >= newrows)
+        term->savecurs.y = newrows - 1;
+    if (term->savecurs.x >= newcols)
+        term->savecurs.x = newcols - 1;
+    if (term->alt_savecurs.y < 0)
+        term->alt_savecurs.y = 0;
+    if (term->alt_savecurs.y >= newrows)
+        term->alt_savecurs.y = newrows - 1;
+    if (term->alt_savecurs.x >= newcols)
+        term->alt_savecurs.x = newcols - 1;
+    if (term->curs.y < 0)
+        term->curs.y = 0;
+    if (term->curs.y >= newrows)
+        term->curs.y = newrows - 1;
+    if (term->curs.x >= newcols)
+        term->curs.x = newcols - 1;
+    if (term->alt_y < 0)
+        term->alt_y = 0;
+    if (term->alt_y >= newrows)
+        term->alt_y = newrows - 1;
+    if (term->alt_x >= newcols)
+        term->alt_x = newcols - 1;
+    term->alt_x = term->alt_y = 0;
+    term->wrapnext = false;
+    term->alt_wnext = false;
+
+    term->rows = newrows;
+    term->cols = newcols;
+    term->savelines = newsavelines;
+
+    swap_screen(term, save_alt_which, false, false);
+
+    term->win_scrollbar_update_pending = true;
+    term_schedule_update(term);
+    if (term->backend)
+        backend_size(term->backend, term->cols, term->rows);
+}
+
+void term_resize_request_completed(Terminal *term)
+{
+    assert(term->win_resize_pending == WIN_RESIZE_AWAIT_REPLY);
+    term->win_resize_pending = WIN_RESIZE_NO;
+    queue_toplevel_callback(term_out_cb, term);
+}
+
+/*
+ * Hand a backend to the terminal, so it can be notified of resizes.
+ */
+void term_provide_backend(Terminal *term, Backend *backend)
+{
+    term->backend = backend;
+    if (term->backend && term->cols > 0 && term->rows > 0)
+        backend_size(term->backend, term->cols, term->rows);
+}
+
+/* Find the bottom line on the screen that has any content.
+ * If only the top line has content, returns 0.
+ * If no lines have content, return -1.
+ */
+static int find_last_nonempty_line(Terminal *term, tree234 *screen)
+{
+    int i;
+    for (i = count234(screen) - 1; i >= 0; i--) {
+        termline *line = index234(screen, i);
+        int j;
+        for (j = 0; j < line->cols; j++)
+            if (!termchars_equal(&line->chars[j], &term->erase_char))
+                break;
+        if (j != line->cols) break;
+    }
+    return i;
+}
+
+/*
+ * Swap screens. If `reset' is true and we have been asked to
+ * switch to the alternate screen, we must bring most of its
+ * configuration from the main screen and erase the contents of the
+ * alternate screen completely. (This is even true if we're already
+ * on it! Blame xterm.)
+ */
+static void swap_screen(Terminal *term, int which,
+                        bool reset, bool keep_cur_pos)
+{
+    int t;
+    bool bt;
+    pos tp;
+    truecolour ttc;
+    tree234 *ttr;
+
+    if (!which)
+        reset = false;                 /* do no weird resetting if which==0 */
+
+    if (which != term->alt_which) {
+        if (term->erase_to_scrollback && term->alt_screen &&
+            term->alt_which && term->disptop < 0) {
+            /*
+             * We're swapping away from the alternate screen, so some
+             * lines are about to vanish from the virtual scrollback.
+             * Adjust disptop by that much, so that (if we're not
+             * resetting the scrollback anyway on a display event) the
+             * current scroll position still ends up pointing at the
+             * same text.
+             */
+            term->disptop += term->alt_sblines;
+            if (term->disptop > 0)
+                term->disptop = 0;
+        }
+
+        term->alt_which = which;
+
+        ttr = term->alt_screen;
+        term->alt_screen = term->screen;
+        term->screen = ttr;
+        term->alt_sblines = (
+            term->alt_screen ?
+            find_last_nonempty_line(term, term->alt_screen) + 1 : 0);
+        t = term->curs.x;
+        if (!reset && !keep_cur_pos)
+            term->curs.x = term->alt_x;
+        term->alt_x = t;
+        t = term->curs.y;
+        if (!reset && !keep_cur_pos)
+            term->curs.y = term->alt_y;
+        term->alt_y = t;
+        t = term->marg_t;
+        if (!reset) term->marg_t = term->alt_t;
+        term->alt_t = t;
+        t = term->marg_b;
+        if (!reset) term->marg_b = term->alt_b;
+        term->alt_b = t;
+        bt = term->dec_om;
+        if (!reset) term->dec_om = term->alt_om;
+        term->alt_om = bt;
+        bt = term->wrap;
+        if (!reset) term->wrap = term->alt_wrap;
+        term->alt_wrap = bt;
+        bt = term->wrapnext;
+        if (!reset) term->wrapnext = term->alt_wnext;
+        term->alt_wnext = bt;
+        bt = term->insert;
+        if (!reset) term->insert = term->alt_ins;
+        term->alt_ins = bt;
+        t = term->cset;
+        if (!reset) term->cset = term->alt_cset;
+        term->alt_cset = t;
+        bt = term->utf;
+        if (!reset) term->utf = term->alt_utf;
+        term->alt_utf = bt;
+        t = term->sco_acs;
+        if (!reset) term->sco_acs = term->alt_sco_acs;
+        term->alt_sco_acs = t;
+
+        tp = term->savecurs;
+        if (!reset)
+            term->savecurs = term->alt_savecurs;
+        term->alt_savecurs = tp;
+        t = term->save_cset;
+        if (!reset)
+            term->save_cset = term->alt_save_cset;
+        term->alt_save_cset = t;
+        t = term->save_csattr;
+        if (!reset)
+            term->save_csattr = term->alt_save_csattr;
+        term->alt_save_csattr = t;
+        t = term->save_attr;
+        if (!reset)
+            term->save_attr = term->alt_save_attr;
+        term->alt_save_attr = t;
+        ttc = term->save_truecolour;
+        if (!reset)
+            term->save_truecolour = term->alt_save_truecolour;
+        term->alt_save_truecolour = ttc;
+        bt = term->save_utf;
+        if (!reset)
+            term->save_utf = term->alt_save_utf;
+        term->alt_save_utf = bt;
+        bt = term->save_wnext;
+        if (!reset)
+            term->save_wnext = term->alt_save_wnext;
+        term->alt_save_wnext = bt;
+        t = term->save_sco_acs;
+        if (!reset)
+            term->save_sco_acs = term->alt_save_sco_acs;
+        term->alt_save_sco_acs = t;
+
+        if (term->erase_to_scrollback && term->alt_screen &&
+            term->alt_which && term->disptop < 0) {
+            /*
+             * Inverse of the adjustment at the top of this function.
+             * This time, we're swapping _to_ the alternate screen, so
+             * some lines are about to _appear_ in the virtual
+             * scrollback, and we adjust disptop in the other
+             * direction.
+             *
+             * Both these adjustments depend on the value stored in
+             * term->alt_sblines while the alt screen is selected,
+             * which is why we had to do one _before_ switching away
+             * from it and the other _after_ switching to it.
+             */
+            term->disptop -= term->alt_sblines;
+            int limit = -sblines(term);
+            if (term->disptop < limit)
+                term->disptop = limit;
+        }
+    }
+
+    if (reset && term->screen) {
+        /*
+         * Yes, this _is_ supposed to honour background-colour-erase.
+         */
+        erase_lots(term, false, true, true);
+    }
+
+    seen_disp_event(term);
+}
+
+/*
+ * Update the scroll bar.
+ */
+static void update_sbar(Terminal *term)
+{
+    int nscroll = sblines(term);
+    win_set_scrollbar(term->win, nscroll + term->rows,
+                      nscroll + term->disptop, term->rows);
+}
+
+/*
+ * Check whether the region bounded by the two pointers intersects
+ * the scroll region, and de-select the on-screen selection if so.
+ */
+static void check_selection(Terminal *term, pos from, pos to)
+{
+    if (poslt(from, term->selend) && poslt(term->selstart, to))
+        deselect(term);
+}
+
+static void clear_line(Terminal *term, termline *line)
+{
+    resizeline(term, line, term->cols);
+    for (int i = 0; i < term->cols; i++)
+        copy_termchar(line, i, &term->erase_char);
+    line->lattr = LATTR_NORM;
+}
+
+static void check_trust_status(Terminal *term, termline *line)
+{
+    if (line->trusted != term->trusted) {
+        /*
+         * If we're displaying trusted output on a previously
+         * untrusted line, or vice versa, we need to switch the
+         * 'trusted' attribute on this terminal line, and also clear
+         * all its previous contents.
+         */
+        clear_line(term, line);
+        line->trusted = term->trusted;
+    }
+}
+
+/*
+ * Scroll the screen. (`lines' is +ve for scrolling forward, -ve
+ * for backward.) `sb' is true if the scrolling is permitted to
+ * affect the scrollback buffer.
+ */
+static void scroll(Terminal *term, int topline, int botline,
+                   int lines, bool sb)
+{
+    termline *line;
+    int seltop, scrollwinsize;
+
+    if (topline != 0 || term->alt_which != 0)
+        sb = false;
+
+    scrollwinsize = botline - topline + 1;
+
+    if (lines < 0) {
+        lines = -lines;
+        if (lines > scrollwinsize)
+            lines = scrollwinsize;
+        while (lines-- > 0) {
+            line = delpos234(term->screen, botline);
+            resizeline(term, line, term->cols);
+            clear_line(term, line);
+            addpos234(term->screen, line, topline);
+
+            if (term->selstart.y >= topline && term->selstart.y <= botline) {
+                term->selstart.y++;
+                if (term->selstart.y > botline) {
+                    term->selstart.y = botline + 1;
+                    term->selstart.x = 0;
+                }
+            }
+            if (term->selend.y >= topline && term->selend.y <= botline) {
+                term->selend.y++;
+                if (term->selend.y > botline) {
+                    term->selend.y = botline + 1;
+                    term->selend.x = 0;
+                }
+            }
+        }
+    } else {
+        if (lines > scrollwinsize)
+            lines = scrollwinsize;
+        while (lines-- > 0) {
+            line = delpos234(term->screen, topline);
+#ifdef TERM_CC_DIAGS
+            cc_check(line);
+#endif
+            if (sb && term->savelines > 0) {
+                int sblen = count234(term->scrollback);
+                /*
+                 * We must add this line to the scrollback. We'll
+                 * remove a line from the top of the scrollback if
+                 * the scrollback is full.
+                 */
+                if (sblen == term->savelines) {
+                    compressed_scrollback_line *cline;
+
+                    sblen--;
+                    cline = delpos234(term->scrollback, 0);
+                    free_compressed_line(cline);
+                } else
+                    term->tempsblines += 1;
+
+                addpos234(term->scrollback, compressline_no_free(line), sblen);
+
+                /* now `line' itself can be reused as the bottom line */
+
+                /*
+                 * If the user is currently looking at part of the
+                 * scrollback, and they haven't enabled any options
+                 * that are going to reset the scrollback as a
+                 * result of this movement, then the chances are
+                 * they'd like to keep looking at the same line. So
+                 * we move their viewpoint at the same rate as the
+                 * scroll, at least until their viewpoint hits the
+                 * top end of the scrollback buffer, at which point
+                 * we don't have the choice any more.
+                 *
+                 * Thanks to Jan Holmen Holsten for the idea and
+                 * initial implementation.
+                 */
+                if (term->disptop > -term->savelines && term->disptop < 0)
+                    term->disptop--;
+
+                /*
+                 * We've just modified the data that the terminal's
+                 * scrollbar is based on, so remember to update it.
+                 */
+                term->win_scrollbar_update_pending = true;
+            }
+            resizeline(term, line, term->cols);
+            clear_line(term, line);
+            line->trusted = false;
+            addpos234(term->screen, line, botline);
+
+            /*
+             * If the selection endpoints move into the scrollback,
+             * we keep them moving until they hit the top. However,
+             * of course, if the line _hasn't_ moved into the
+             * scrollback then we don't do this, and cut them off
+             * at the top of the scroll region.
+             *
+             * This applies to selstart and selend (for an existing
+             * selection), and also selanchor (for one being
+             * selected as we speak).
+             */
+            seltop = sb ? -term->savelines : topline;
+
+            if (term->selstate != NO_SELECTION) {
+                if (term->selstart.y >= seltop &&
+                    term->selstart.y <= botline) {
+                    term->selstart.y--;
+                    if (term->selstart.y < seltop) {
+                        term->selstart.y = seltop;
+                        term->selstart.x = 0;
+                    }
+                }
+                if (term->selend.y >= seltop && term->selend.y <= botline) {
+                    term->selend.y--;
+                    if (term->selend.y < seltop) {
+                        term->selend.y = seltop;
+                        term->selend.x = 0;
+                    }
+                }
+                if (term->selanchor.y >= seltop &&
+                    term->selanchor.y <= botline) {
+                    term->selanchor.y--;
+                    if (term->selanchor.y < seltop) {
+                        term->selanchor.y = seltop;
+                        term->selanchor.x = 0;
+                    }
+                }
+            }
+        }
+    }
+
+    seen_disp_event(term);
+}
+
+/*
+ * Move the cursor to a given position, clipping at boundaries. We
+ * may or may not want to clip at the scroll margin: marg_clip is 0
+ * not to, 1 to disallow _passing_ the margins, and 2 to disallow
+ * even _being_ outside the margins.
+ */
+static void move(Terminal *term, int x, int y, int marg_clip)
+{
+    if (x < 0)
+        x = 0;
+    if (x >= term->cols)
+        x = term->cols - 1;
+    if (marg_clip) {
+        if ((term->curs.y >= term->marg_t || marg_clip == 2) &&
+            y < term->marg_t)
+            y = term->marg_t;
+        if ((term->curs.y <= term->marg_b || marg_clip == 2) &&
+            y > term->marg_b)
+            y = term->marg_b;
+    }
+    if (y < 0)
+        y = 0;
+    if (y >= term->rows)
+        y = term->rows - 1;
+    term->curs.x = x;
+    term->curs.y = y;
+    term->wrapnext = false;
+    seen_disp_event(term);
+}
+
+/*
+ * Save or restore the cursor and SGR mode.
+ */
+static void save_cursor(Terminal *term, bool save)
+{
+    if (save) {
+        term->savecurs = term->curs;
+        term->save_attr = term->curr_attr;
+        term->save_truecolour = term->curr_truecolour;
+        term->save_cset = term->cset;
+        term->save_utf = term->utf;
+        term->save_wnext = term->wrapnext;
+        term->save_csattr = term->cset_attr[term->cset];
+        term->save_sco_acs = term->sco_acs;
+    } else {
+        term->curs = term->savecurs;
+        /* Make sure the window hasn't shrunk since the save */
+        if (term->curs.x >= term->cols)
+            term->curs.x = term->cols - 1;
+        if (term->curs.y >= term->rows)
+            term->curs.y = term->rows - 1;
+
+        term->curr_attr = term->save_attr;
+        term->curr_truecolour = term->save_truecolour;
+        term->cset = term->save_cset;
+        term->utf = term->save_utf;
+        term->wrapnext = term->save_wnext;
+        /*
+         * wrapnext might reset to False if the x position is no
+         * longer at the rightmost edge.
+         */
+        if (term->wrapnext && term->curs.x < term->cols-1)
+            term->wrapnext = false;
+        term->cset_attr[term->cset] = term->save_csattr;
+        term->sco_acs = term->save_sco_acs;
+        set_erase_char(term);
+        seen_disp_event(term);
+    }
+}
+
+/*
+ * This function is called before doing _anything_ which affects
+ * only part of a line of text. It is used to mark the boundary
+ * between two character positions, and it indicates that some sort
+ * of effect is going to happen on only one side of that boundary.
+ *
+ * The effect of this function is to check whether a CJK
+ * double-width character is straddling the boundary, and to remove
+ * it and replace it with two spaces if so. (Of course, one or
+ * other of those spaces is then likely to be replaced with
+ * something else again, as a result of whatever happens next.)
+ *
+ * Also, if the boundary is at the right-hand _edge_ of the screen,
+ * it implies something deliberate is being done to the rightmost
+ * column position; hence we must clear LATTR_WRAPPED2.
+ *
+ * The input to the function is the coordinates of the _second_
+ * character of the pair.
+ */
+static void check_boundary(Terminal *term, int x, int y)
+{
+    termline *ldata;
+
+    /* Validate input coordinates, just in case. */
+    if (x <= 0 || x > term->cols)
+        return;
+
+    ldata = scrlineptr(y);
+    check_trust_status(term, ldata);
+    check_line_size(term, ldata);
+    if (x == term->cols) {
+        ldata->lattr &= ~LATTR_WRAPPED2;
+    } else {
+        if (ldata->chars[x].chr == UCSWIDE) {
+            clear_cc(ldata, x-1);
+            clear_cc(ldata, x);
+            ldata->chars[x-1].chr = ' ' | CSET_ASCII;
+            ldata->chars[x] = ldata->chars[x-1];
+        }
+    }
+}
+
+/*
+ * Erase a large portion of the screen: the whole screen, or the
+ * whole line, or parts thereof.
+ */
+static void erase_lots(Terminal *term,
+                       bool line_only, bool from_begin, bool to_end)
+{
+    pos start, end;
+    bool erase_lattr;
+    bool erasing_lines_from_top = false;
+
+    if (line_only) {
+        start.y = term->curs.y;
+        start.x = 0;
+        end.y = term->curs.y + 1;
+        end.x = 0;
+        erase_lattr = false;
+    } else {
+        start.y = 0;
+        start.x = 0;
+        end.y = term->rows;
+        end.x = 0;
+        erase_lattr = true;
+    }
+
+    /* This is the endpoint of the clearing operation that is not
+     * either the start or end of the line / screen. */
+    pos boundary = term->curs;
+
+    if (!from_begin) {
+        /*
+         * If we're erasing from the current char to the end of
+         * line/screen, then we take account of wrapnext, so as to
+         * maintain the invariant that writing a printing character
+         * followed by ESC[K should not overwrite the character you
+         * _just wrote_. That is, when wrapnext says the cursor is
+         * 'logically' at the very rightmost edge of the screen
+         * instead of just before the last printing char, ESC[K should
+         * do nothing at all, and ESC[J should clear the next line but
+         * leave this one unchanged.
+         *
+         * This adjusted position will also be the position we use for
+         * check_boundary (i.e. the thing we ensure isn't in the
+         * middle of a double-width printing char).
+         */
+        if (term->wrapnext)
+            incpos(boundary);
+
+        start = boundary;
+    }
+    if (!to_end) {
+        /*
+         * If we're erasing from the start of (at least) the line _to_
+         * the current position, then that is taken to mean 'inclusive
+         * of the cell under the cursor', which means we don't
+         * consider wrapnext at all: whether it's set or not, we still
+         * clear the cell under the cursor.
+         *
+         * Again, that incremented boundary position is where we
+         * should be careful of a straddling wide character.
+         */
+        incpos(boundary);
+        end = boundary;
+    }
+    if (!from_begin || !to_end)
+        check_boundary(term, boundary.x, boundary.y);
+    check_selection(term, start, end);
+
+    /* Clear screen also forces a full window redraw, just in case. */
+    if (start.y == 0 && start.x == 0 && end.y == term->rows)
+        term_invalidate(term);
+
+    /* Lines scrolled away shouldn't be brought back on if the terminal
+     * resizes. */
+    if (start.y == 0 && start.x == 0 && end.x == 0 && erase_lattr)
+        erasing_lines_from_top = true;
+
+    if (term->erase_to_scrollback && erasing_lines_from_top) {
+        /* If it's a whole number of lines, starting at the top, and
+         * we're fully erasing them, erase by scrolling and keep the
+         * lines in the scrollback. */
+        int scrolllines = end.y;
+        if (end.y == term->rows) {
+            /* Shrink until we find a non-empty row.*/
+            scrolllines = find_last_nonempty_line(term, term->screen) + 1;
+        }
+        if (scrolllines > 0)
+            scroll(term, 0, scrolllines - 1, scrolllines, true);
+    } else {
+        termline *ldata = scrlineptr(start.y);
+        check_trust_status(term, ldata);
+        while (poslt(start, end)) {
+            check_line_size(term, ldata);
+            if (start.x == term->cols) {
+                if (!erase_lattr)
+                    ldata->lattr &= ~(LATTR_WRAPPED | LATTR_WRAPPED2);
+                else
+                    ldata->lattr = LATTR_NORM;
+            } else {
+                copy_termchar(ldata, start.x, &term->erase_char);
+            }
+            if (incpos(start) && start.y < term->rows) {
+                ldata = scrlineptr(start.y);
+                check_trust_status(term, ldata);
+            }
+        }
+    }
+
+    /* After an erase of lines from the top of the screen, we shouldn't
+     * bring the lines back again if the terminal enlarges (since the user or
+     * application has explicitly thrown them away). */
+    if (erasing_lines_from_top && !(term->alt_which))
+        term->tempsblines = 0;
+
+    seen_disp_event(term);
+}
+
+/*
+ * Insert or delete characters within the current line. n is +ve if
+ * insertion is desired, and -ve for deletion.
+ */
+static void insch(Terminal *term, int n)
+{
+    int dir = (n < 0 ? -1 : +1);
+    int m, j;
+    pos eol;
+    termline *ldata;
+
+    n = (n < 0 ? -n : n);
+    if (n > term->cols - term->curs.x)
+        n = term->cols - term->curs.x;
+    m = term->cols - term->curs.x - n;
+
+    /*
+     * We must de-highlight the selection if it overlaps any part of
+     * the region affected by this operation, i.e. the region from the
+     * current cursor position to end-of-line, _unless_ the entirety
+     * of the selection is going to be moved to the left or right by
+     * this operation but otherwise unchanged, in which case we can
+     * simply move the highlight with the text.
+     */
+    eol.y = term->curs.y;
+    eol.x = term->cols;
+    if (poslt(term->curs, term->selend) && poslt(term->selstart, eol)) {
+        pos okstart = term->curs;
+        pos okend = eol;
+        if (dir > 0) {
+            /* Insertion: n characters at EOL will be splatted. */
+            okend.x -= n;
+        } else {
+            /* Deletion: n characters at cursor position will be splatted. */
+            okstart.x += n;
+        }
+        if (posle(okstart, term->selstart) && posle(term->selend, okend)) {
+            /* Selection is contained entirely in the interval
+             * [okstart,okend), so we need only adjust the selection
+             * bounds. */
+            term->selstart.x += dir * n;
+            term->selend.x += dir * n;
+            assert(term->selstart.x >= term->curs.x);
+            assert(term->selstart.x < term->cols);
+            assert(term->selend.x > term->curs.x);
+            assert(term->selend.x <= term->cols);
+        } else {
+            /* Selection is not wholly contained in that interval, so
+             * we must unhighlight it. */
+            deselect(term);
+        }
+    }
+
+    check_boundary(term, term->curs.x, term->curs.y);
+    if (dir < 0)
+        check_boundary(term, term->curs.x + n, term->curs.y);
+    ldata = scrlineptr(term->curs.y);
+    check_trust_status(term, ldata);
+    if (dir < 0) {
+        for (j = 0; j < m; j++)
+            move_termchar(ldata,
+                          ldata->chars + term->curs.x + j,
+                          ldata->chars + term->curs.x + j + n);
+        while (n--)
+            copy_termchar(ldata, term->curs.x + m++, &term->erase_char);
+    } else {
+        for (j = m; j-- ;)
+            move_termchar(ldata,
+                          ldata->chars + term->curs.x + j + n,
+                          ldata->chars + term->curs.x + j);
+        while (n--)
+            copy_termchar(ldata, term->curs.x + n, &term->erase_char);
+    }
+}
+
+static void term_update_raw_mouse_mode(Terminal *term)
+{
+    bool want_raw = (term->xterm_mouse != 0 && !term->xterm_mouse_forbidden);
+    win_set_raw_mouse_mode(term->win, want_raw);
+    term->win_pointer_shape_pending = true;
+    term->win_pointer_shape_raw = want_raw;
+    term_schedule_update(term);
+}
+
+static void term_request_resize(Terminal *term, int cols, int rows)
+{
+    if (term->cols == cols && term->rows == rows)
+        return;                        /* don't need to do anything */
+
+    term->win_resize_pending = WIN_RESIZE_NEED_SEND;
+    term->win_resize_pending_w = cols;
+    term->win_resize_pending_h = rows;
+    term_schedule_update(term);
+}
+
+/*
+ * Toggle terminal mode `mode' to state `state'. (`query' indicates
+ * whether the mode is a DEC private one or a normal one.)
+ */
+static void toggle_mode(Terminal *term, int mode, int query, bool state)
+{
+    if (query == 1) {
+        switch (mode) {
+          case 1:                      /* DECCKM: application cursor keys */
+            term->app_cursor_keys = state;
+            break;
+          case 2:                      /* DECANM: VT52 mode */
+            term->vt52_mode = !state;
+            if (term->vt52_mode) {
+                term->blink_is_real = false;
+                term->vt52_bold = false;
+            } else {
+                term->blink_is_real = term->blinktext;
+            }
+            term_schedule_tblink(term);
+            break;
+          case 3:                      /* DECCOLM: 80/132 columns */
+            deselect(term);
+            if (!term->no_remote_resize)
+                term_request_resize(term, state ? 132 : 80, term->rows);
+            term->reset_132 = state;
+            term->alt_t = term->marg_t = 0;
+            term->alt_b = term->marg_b = term->rows - 1;
+            move(term, 0, 0, 0);
+            erase_lots(term, false, true, true);
+            break;
+          case 5:                      /* DECSCNM: reverse video */
+            /*
+             * Toggle reverse video. If we receive an OFF within the
+             * visual bell timeout period after an ON, we trigger an
+             * effective visual bell, so that ESC[?5hESC[?5l will
+             * always be an actually _visible_ visual bell.
+             */
+            if (term->rvideo && !state) {
+                /* This is an OFF, so set up a vbell */
+                term_schedule_vbell(term, true, term->rvbell_startpoint);
+            } else if (!term->rvideo && state) {
+                /* This is an ON, so we notice the time and save it. */
+                term->rvbell_startpoint = GETTICKCOUNT();
+            }
+            term->rvideo = state;
+            seen_disp_event(term);
+            break;
+          case 6:                      /* DECOM: DEC origin mode */
+            term->dec_om = state;
+            break;
+          case 7:                      /* DECAWM: auto wrap */
+            term->wrap = state;
+            break;
+          case 8:                      /* DECARM: auto key repeat */
+            term->repeat_off = !state;
+            break;
+          case 25:                     /* DECTCEM: enable/disable cursor */
+            compatibility2(OTHER, VT220);
+            term->cursor_on = state;
+            seen_disp_event(term);
+            break;
+          case 47:                     /* alternate screen */
+            compatibility(OTHER);
+            deselect(term);
+            swap_screen(term, term->no_alt_screen ? 0 : state, false, false);
+            if (term->scroll_on_disp)
+                term->disptop = 0;
+            break;
+          case 1000:                   /* xterm mouse 1 (normal) */
+            term->xterm_mouse = state ? 1 : 0;
+            term_update_raw_mouse_mode(term);
+            break;
+          case 1002:                   /* xterm mouse 2 (inc. button drags) */
+            term->xterm_mouse = state ? 2 : 0;
+            term_update_raw_mouse_mode(term);
+            break;
+          case 1003:                   /* xterm mouse any-event tracking */
+            term->xterm_mouse = state ? 3 : 0;
+            term_update_raw_mouse_mode(term);
+            break;
+          case 1006:                   /* xterm extended mouse */
+            term->xterm_extended_mouse = state;
+            break;
+          case 1015:                   /* urxvt extended mouse */
+            term->urxvt_extended_mouse = state;
+            break;
+          case 1047:                   /* alternate screen */
+            compatibility(OTHER);
+            deselect(term);
+            swap_screen(term, term->no_alt_screen ? 0 : state, true, true);
+            if (term->scroll_on_disp)
+                term->disptop = 0;
+            break;
+          case 1048:                   /* save/restore cursor */
+            if (!term->no_alt_screen)
+                save_cursor(term, state);
+            if (!state) seen_disp_event(term);
+            break;
+          case 1049:                   /* cursor & alternate screen */
+            if (state && !term->no_alt_screen)
+                save_cursor(term, state);
+            if (!state) seen_disp_event(term);
+            compatibility(OTHER);
+            deselect(term);
+            swap_screen(term, term->no_alt_screen ? 0 : state, true, false);
+            if (!state && !term->no_alt_screen)
+                save_cursor(term, state);
+            if (term->scroll_on_disp)
+                term->disptop = 0;
+            break;
+          case 2004:                   /* xterm bracketed paste */
+            term->bracketed_paste = state ? true : false;
+            break;
+        }
+    } else if (query == 0) {
+        switch (mode) {
+          case 4:                      /* IRM: set insert mode */
+            compatibility(VT102);
+            term->insert = state;
+            break;
+          case 12:                     /* SRM: set echo mode */
+            term->srm_echo = !state;
+            break;
+          case 20:                     /* LNM: Return sends ... */
+            term->cr_lf_return = state;
+            break;
+          case 34:                     /* WYULCURM: Make cursor BIG */
+            compatibility2(OTHER, VT220);
+            term->big_cursor = !state;
+        }
+    }
+}
+
+/*
+ * Process an OSC sequence: set window title or icon name.
+ */
+static void do_osc(Terminal *term)
+{
+    if (term->osc_is_apc) {
+        /* This OSC was really an APC, and we don't support that
+         * sequence at all. We only recognise it in order to ignore it
+         * and filter it out of input. */
+        return;
+    }
+
+    if (term->osc_w) {
+        while (term->osc_strlen--)
+            term->wordness[(unsigned char)term->osc_string[term->osc_strlen]] =
+                term->esc_args[0];
+    } else {
+        term->osc_string[term->osc_strlen] = '\0';
+        switch (term->esc_args[0]) {
+          case 0:
+          case 1:
+            if (!term->no_remote_wintitle) {
+                sfree(term->icon_title);
+                term->icon_title = dupstr(term->osc_string);
+                term->icontitle_codepage = term->ucsdata->line_codepage;
+                term->win_icon_title_pending = true;
+                term_schedule_update(term);
+            }
+            if (term->esc_args[0] == 1)
+                break;
+            /* fall through: parameter 0 means set both */
+          case 2:
+          case 21:
+            if (!term->no_remote_wintitle) {
+                sfree(term->window_title);
+                term->window_title = dupstr(term->osc_string);
+                term->wintitle_codepage = term->ucsdata->line_codepage;
+                term->win_title_pending = true;
+                term_schedule_update(term);
+            }
+            break;
+          case 4:
+            if (term->ldisc && !strcmp(term->osc_string, "?")) {
+                unsigned index = term->esc_args[1];
+                if (index < OSC4_NCOLOURS) {
+                    rgb colour = term->palette[index];
+                    char *reply_buf = dupprintf(
+                        "\033]4;%u;rgb:%04x/%04x/%04x\007", index,
+                        (unsigned)colour.r * 0x0101,
+                        (unsigned)colour.g * 0x0101,
+                        (unsigned)colour.b * 0x0101);
+                    ldisc_send(term->ldisc, reply_buf, strlen(reply_buf),
+                               false);
+                    sfree(reply_buf);
+                }
+            }
+            break;
+        }
+    }
+}
+
+/*
+ * ANSI printing routines.
+ */
+static void term_print_setup(Terminal *term, char *printer)
+{
+    bufchain_clear(&term->printer_buf);
+    term->print_job = printer_start_job(printer);
+}
+static void term_print_flush(Terminal *term)
+{
+    size_t size;
+    while ((size = bufchain_size(&term->printer_buf)) > 5) {
+        ptrlen data = bufchain_prefix(&term->printer_buf);
+        if (data.len > size-5)
+            data.len = size-5;
+        printer_job_data(term->print_job, data.ptr, data.len);
+        bufchain_consume(&term->printer_buf, data.len);
+    }
+}
+static void term_print_finish(Terminal *term)
+{
+    size_t size;
+    char c;
+
+    if (!term->printing && !term->only_printing)
+        return;                        /* we need do nothing */
+
+    term_print_flush(term);
+    while ((size = bufchain_size(&term->printer_buf)) > 0) {
+        ptrlen data = bufchain_prefix(&term->printer_buf);
+        c = *(char *)data.ptr;
+        if (c == '\033' || c == '\233') {
+            bufchain_consume(&term->printer_buf, size);
+            break;
+        } else {
+            printer_job_data(term->print_job, &c, 1);
+            bufchain_consume(&term->printer_buf, 1);
+        }
+    }
+    printer_finish_job(term->print_job);
+    term->print_job = NULL;
+    term->printing = term->only_printing = false;
+}
+
+static void term_display_graphic_char(Terminal *term, unsigned long c)
+{
+    termline *cline = scrlineptr(term->curs.y);
+    int width = 0;
+    if (DIRECT_CHAR(c))
+        width = 1;
+    if (!width)
+        width = term_char_width(term, c);
+
+    if (term->wrapnext && term->wrap && width > 0) {
+        cline->lattr |= LATTR_WRAPPED;
+        if (term->curs.y == term->marg_b)
+            scroll(term, term->marg_t, term->marg_b, 1, true);
+        else if (term->curs.y < term->rows - 1)
+            term->curs.y++;
+        term->curs.x = 0;
+        term->wrapnext = false;
+        cline = scrlineptr(term->curs.y);
+    }
+    if (term->insert && width > 0)
+        insch(term, width);
+    if (term->selstate != NO_SELECTION) {
+        pos cursplus = term->curs;
+        incpos(cursplus);
+        check_selection(term, term->curs, cursplus);
+    }
+    if (((c & CSET_MASK) == CSET_ASCII ||
+         (c & CSET_MASK) == 0) && term->logctx)
+        logtraffic(term->logctx, (unsigned char) c, LGTYP_ASCII);
+
+    check_trust_status(term, cline);
+
+    int linecols = term->cols;
+    if (cline->trusted)
+        linecols -= TRUST_SIGIL_WIDTH;
+
+    /*
+     * Before we switch on the character width, do a preliminary check for
+     * cases where we might have no room at all to display a double-width
+     * character. Our fallback is to substitute REPLACEMENT CHARACTER,
+     * which is single-width, and it's easiest to do that _before_ having
+     * to 'goto' from one switch case to another.
+     */
+    if (width == 2 && term->curs.x >= linecols-1) {
+        /*
+         * If we're in wrapping mode and the terminal is at least 2 cells
+         * wide, it's OK, we have a fallback. But otherwise, substitute.
+         */
+        if (linecols < 2 || !term->wrap) {
+            width = 1;
+            c = 0xFFFD;
+        }
+    }
+
+    switch (width) {
+      case 2:
+        /*
+         * If we're about to display a double-width character starting in
+         * the rightmost column (and we're in wrapping mode - the other
+         * case was disposed of above), then we do something special
+         * instead. We must print a space in the last column of the screen,
+         * then wrap; and we also set LATTR_WRAPPED2 which instructs
+         * subsequent cut-and-pasting not only to splice this line to the
+         * one after it, but to ignore the space in the last character
+         * position as well. (Because what was actually output to the
+         * terminal was presumably just a sequence of CJK characters, and
+         * we don't want a space to be pasted in the middle of those just
+         * because they had the misfortune to start in the wrong parity
+         * column. xterm concurs.)
+         */
+        check_boundary(term, term->curs.x, term->curs.y);
+        check_boundary(term, term->curs.x+2, term->curs.y);
+        if (term->curs.x >= linecols-1) {
+            assert(term->wrap);    /* we handled the non-wrapping case above */
+            copy_termchar(cline, term->curs.x,
+                          &term->erase_char);
+            cline->lattr |= LATTR_WRAPPED | LATTR_WRAPPED2;
+            if (term->curs.y == term->marg_b)
+                scroll(term, term->marg_t, term->marg_b,
+                       1, true);
+            else if (term->curs.y < term->rows - 1)
+                term->curs.y++;
+            term->curs.x = 0;
+            cline = scrlineptr(term->curs.y);
+            /* Now we must check_boundary again, of course. */
+            check_boundary(term, term->curs.x, term->curs.y);
+            check_boundary(term, term->curs.x+2, term->curs.y);
+        }
+
+        /* FULL-TERMCHAR */
+        clear_cc(cline, term->curs.x);
+        cline->chars[term->curs.x].chr = c;
+        cline->chars[term->curs.x].attr = term->curr_attr;
+        cline->chars[term->curs.x].truecolour =
+            term->curr_truecolour;
+
+        term->curs.x++;
+
+        /* FULL-TERMCHAR */
+        clear_cc(cline, term->curs.x);
+        cline->chars[term->curs.x].chr = UCSWIDE;
+        cline->chars[term->curs.x].attr = term->curr_attr;
+        cline->chars[term->curs.x].truecolour =
+            term->curr_truecolour;
+
+        break;
+      case 1:
+        check_boundary(term, term->curs.x, term->curs.y);
+        check_boundary(term, term->curs.x+1, term->curs.y);
+
+        /* FULL-TERMCHAR */
+        clear_cc(cline, term->curs.x);
+        cline->chars[term->curs.x].chr = c;
+        cline->chars[term->curs.x].attr = term->curr_attr;
+        cline->chars[term->curs.x].truecolour =
+            term->curr_truecolour;
+
+        break;
+      case 0:
+        if (term->curs.x > 0) {
+            int x = term->curs.x - 1;
+
+            /* If we're in wrapnext state, the character to combine
+             * with is _here_, not to our left. */
+            if (term->wrapnext)
+                x++;
+
+            /*
+             * If the previous character is UCSWIDE, back up another
+             * one.
+             */
+            if (cline->chars[x].chr == UCSWIDE) {
+                assert(x > 0);
+                x--;
+            }
+
+            add_cc(cline, x, c);
+            seen_disp_event(term);
+        }
+        return;
+      default:
+        return;
+    }
+    term->curs.x++;
+    if (term->curs.x >= linecols) {
+        term->curs.x = linecols - 1;
+        term->wrapnext = true;
+        if (term->wrap && term->vt52_mode) {
+            cline->lattr |= LATTR_WRAPPED;
+            if (term->curs.y == term->marg_b)
+                scroll(term, term->marg_t, term->marg_b, 1, true);
+            else if (term->curs.y < term->rows - 1)
+                term->curs.y++;
+            term->curs.x = 0;
+            term->wrapnext = false;
+        }
+    }
+    seen_disp_event(term);
+}
+
+static strbuf *term_input_data_from_unicode(
+    Terminal *term, const wchar_t *widebuf, int len)
+{
+    strbuf *buf = strbuf_new();
+
+    if (in_utf(term)) {
+        /*
+         * Translate input wide characters into UTF-8 to go in the
+         * terminal's input data queue.
+         */
+        for (int i = 0; i < len; i++) {
+            unsigned long ch = widebuf[i];
+
+            if (IS_SURROGATE(ch)) {
+#ifdef PLATFORM_IS_UTF16
+                if (i+1 < len) {
+                    unsigned long ch2 = widebuf[i+1];
+                    if (IS_SURROGATE_PAIR(ch, ch2)) {
+                        ch = FROM_SURROGATES(ch, ch2);
+                        i++;
+                    }
+                } else
+#endif
+                {
+                    /* Unrecognised UTF-16 sequence */
+                    ch = '.';
+                }
+            }
+
+            char utf8_chr[6];
+            put_data(buf, utf8_chr, encode_utf8(utf8_chr, ch));
+        }
+    } else {
+        /*
+         * Call to the character-set subsystem to translate into
+         * whatever charset the terminal is currently configured in.
+         *
+         * Since the terminal doesn't currently support any multibyte
+         * character set other than UTF-8, we can assume here that
+         * there will be at most one output byte per input wchar_t.
+         * (But also we must allow space for the trailing NUL that
+         * wc_to_mb will write.)
+         */
+        char *bufptr = strbuf_append(buf, len + 1);
+        int rv;
+        rv = wc_to_mb(term->ucsdata->line_codepage, 0, widebuf, len,
+                      bufptr, len + 1, NULL);
+        strbuf_shrink_to(buf, rv < 0 ? 0 : rv);
+    }
+
+    return buf;
+}
+
+static strbuf *term_input_data_from_charset(
+    Terminal *term, int codepage, const char *str, int len)
+{
+    strbuf *buf;
+
+    if (codepage < 0) {
+        buf = strbuf_new();
+        put_data(buf, str, len);
+    } else {
+        int widesize = len * 2;        /* allow for UTF-16 surrogates */
+        wchar_t *widebuf = snewn(widesize, wchar_t);
+        int widelen = mb_to_wc(codepage, 0, str, len, widebuf, widesize);
+        buf = term_input_data_from_unicode(term, widebuf, widelen);
+        sfree(widebuf);
+    }
+
+    return buf;
+}
+
+static inline void term_bracketed_paste_start(Terminal *term)
+{
+    ptrlen seq = PTRLEN_LITERAL("\033[200~");
+    if (term->ldisc)
+        ldisc_send(term->ldisc, seq.ptr, seq.len, false);
+    term->bracketed_paste_active = true;
+}
+
+static inline void term_bracketed_paste_stop(Terminal *term)
+{
+    if (!term->bracketed_paste_active)
+        return;
+
+    ptrlen seq = PTRLEN_LITERAL("\033[201~");
+    if (term->ldisc)
+        ldisc_send(term->ldisc, seq.ptr, seq.len, false);
+    term->bracketed_paste_active = false;
+}
+
+static inline void term_keyinput_internal(
+    Terminal *term, const void *buf, int len, bool interactive)
+{
+    if (term->srm_echo) {
+        /*
+         * Implement the terminal-level local echo behaviour that
+         * ECMA-48 specifies when terminal mode 12 is configured off
+         * (ESC[12l). In this mode, data input to the terminal via the
+         * keyboard is also added to the output buffer. But this
+         * doesn't apply to escape sequences generated as session
+         * input _within_ the terminal, e.g. in response to terminal
+         * query sequences, or the bracketing sequences of bracketed
+         * paste mode. Those will be sent directly via
+         * ldisc_send(term->ldisc, ...) and won't go through this
+         * function.
+         */
+
+        /* Mimic the special case of negative length in ldisc_send */
+        int true_len = len >= 0 ? len : strlen(buf);
+
+        bufchain_add(&term->inbuf, buf, true_len);
+        term_added_data(term, false);
+    }
+    if (interactive)
+        term_bracketed_paste_stop(term);
+    if (term->ldisc)
+        ldisc_send(term->ldisc, buf, len, interactive);
+    term_seen_key_event(term);
+}
+
+unsigned long term_translate(
+    Terminal *term, struct term_utf8_decode *utf8, unsigned char c)
+{
+    if (in_utf(term)) {
+        switch (utf8->state) {
+          case 0:
+            if (c < 0x80) {
+                /* UTF-8 must be stateless so we ignore iso2022. */
+                if (term->ucsdata->unitab_ctrl[c] != 0xFF)  {
+                    return term->ucsdata->unitab_ctrl[c];
+                } else if ((term->utf8linedraw) &&
+                           (term->cset_attr[term->cset] == CSET_LINEDRW)) {
+                    /* Linedraw characters are explicitly enabled */
+                    return c | CSET_LINEDRW;
+                } else {
+                    return c | CSET_ASCII;
+                }
+            } else if ((c & 0xe0) == 0xc0) {
+                utf8->size = utf8->state = 1;
+                utf8->chr = (c & 0x1f);
+            } else if ((c & 0xf0) == 0xe0) {
+                utf8->size = utf8->state = 2;
+                utf8->chr = (c & 0x0f);
+            } else if ((c & 0xf8) == 0xf0) {
+                utf8->size = utf8->state = 3;
+                utf8->chr = (c & 0x07);
+            } else if ((c & 0xfc) == 0xf8) {
+                utf8->size = utf8->state = 4;
+                utf8->chr = (c & 0x03);
+            } else if ((c & 0xfe) == 0xfc) {
+                utf8->size = utf8->state = 5;
+                utf8->chr = (c & 0x01);
+            } else {
+                return UCSINVALID;
+            }
+            return UCSINCOMPLETE;
+          case 1:
+          case 2:
+          case 3:
+          case 4:
+          case 5:
+            if ((c & 0xC0) != 0x80) {
+                utf8->state = 0;
+                return UCSTRUNCATED;   /* caller will then give us the
+                                        * same byte again */
+            }
+            utf8->chr = (utf8->chr << 6) | (c & 0x3f);
+            if (--utf8->state)
+                return UCSINCOMPLETE;
+
+            unsigned long t = utf8->chr;
+
+            /* Is somebody trying to be evil! */
+            if (t < 0x80 ||
+                (t < 0x800 && utf8->size >= 2) ||
+                (t < 0x10000 && utf8->size >= 3) ||
+                (t < 0x200000 && utf8->size >= 4) ||
+                (t < 0x4000000 && utf8->size >= 5))
+                return UCSINVALID;
+
+            /* Unicode line separator and paragraph separator are CR-LF */
+            if (t == 0x2028 || t == 0x2029)
+                return 0x85;
+
+            /* High controls are probably a Baaad idea too. */
+            if (t < 0xA0)
+                return 0xFFFD;
+
+            /* The UTF-16 surrogates are not nice either. */
+            /*       The standard give the option of decoding these:
+             *       I don't want to! */
+            if (t >= 0xD800 && t < 0xE000)
+                return UCSINVALID;
+
+            /* ISO 10646 characters now limited to UTF-16 range. */
+            if (t > 0x10FFFF)
+                return UCSINVALID;
+
+            /* This is currently a TagPhobic application.. */
+            if (t >= 0xE0000 && t <= 0xE007F)
+                return UCSINCOMPLETE;
+
+            /* U+FEFF is best seen as a null. */
+            if (t == 0xFEFF)
+                return UCSINCOMPLETE;
+            /* But U+FFFE is an error. */
+            if (t == 0xFFFE || t == 0xFFFF)
+                return UCSINVALID;
+
+            return t;
+        }
+    } else if (term->sco_acs &&
+               (c!='\033' && c!='\012' && c!='\015' && c!='\b')) {
+        /* Are we in the nasty ACS mode? Note: no sco in utf mode. */
+        if (term->sco_acs == 2)
+            c |= 0x80;
+
+        return c | CSET_SCOACS;
+    } else {
+        switch (term->cset_attr[term->cset]) {
+            /*
+             * Linedraw characters are different from 'ESC ( B'
+             * only for a small range. For ones outside that
+             * range, make sure we use the same font as well as
+             * the same encoding.
+             */
+          case CSET_LINEDRW:
+            if (term->ucsdata->unitab_ctrl[c] != 0xFF)
+                return term->ucsdata->unitab_ctrl[c];
+            else
+                return c | CSET_LINEDRW;
+            break;
+
+          case CSET_GBCHR:
+            /* If UK-ASCII, make the '#' a LineDraw Pound */
+            if (c == '#')
+                return '}' | CSET_LINEDRW;
+            /* fall through */
+
+          case CSET_ASCII:
+            if (term->ucsdata->unitab_ctrl[c] != 0xFF)
+                return term->ucsdata->unitab_ctrl[c];
+            else
+                return c | CSET_ASCII;
+            break;
+          case CSET_SCOACS:
+            if (c >= ' ')
+                return c | CSET_SCOACS;
+            break;
+        }
+    }
+    return c;
+}
+
+/*
+ * Remove everything currently in `inbuf' and stick it up on the
+ * in-memory display. There's a big state machine in here to
+ * process escape sequences...
+ */
+static void term_out(Terminal *term, bool called_from_term_data)
+{
+    unsigned long c;
+    int unget;
+    const unsigned char *chars;
+    size_t nchars_got = 0, nchars_used = 0;
+
+    /*
+     * During drag-selects, we do not process terminal input, because
+     * the user will want the screen to hold still to be selected.
+     */
+    if (term->selstate == DRAGGING)
+        return;
+
+    unget = -1;
+
+    chars = NULL;                      /* placate compiler warnings */
+    while (nchars_got < nchars_used ||
+           unget != -1 ||
+           bufchain_size(&term->inbuf) > 0) {
+        if (unget != -1) {
+            /*
+             * Handle a character we left in 'unget' the last time
+             * round this loop. This happens if a UTF-8 sequence is
+             * aborted early, by containing fewer continuation bytes
+             * than its introducer expected: the non-continuation byte
+             * that interrupted the sequence must now be processed
+             * as a fresh piece of input in its own right.
+             */
+            c = unget;
+            unget = -1;
+        } else {
+            /*
+             * If we're waiting for a terminal resize triggered by an
+             * escape sequence, we defer processing the terminal
+             * output until we receive acknowledgment from the front
+             * end that the resize has happened, so that further
+             * output will be processed in the context of the new
+             * size.
+             *
+             * This test goes inside the main while-loop, so that we
+             * exit early if we encounter a resize escape sequence
+             * part way through term->inbuf.
+             *
+             * It's also in the branch of this if statement that
+             * doesn't deal with a character left in 'unget' by the
+             * previous loop iteration, because if we break out of
+             * this loop with an ungot character still pending, we'll
+             * lose it. (And in any case, if the previous thing that
+             * happened was a truncated UTF-8 sequence, then it won't
+             * have scheduled a pending resize.)
+             */
+            if (term->win_resize_pending != WIN_RESIZE_NO)
+                break;
+
+            if (nchars_got == nchars_used) {
+                /* Delete the previous chunk from the bufchain */
+                bufchain_consume(&term->inbuf, nchars_used);
+                nchars_used = 0;
+
+                if (bufchain_size(&term->inbuf) == 0)
+                    break;             /* no more data */
+
+                ptrlen data = bufchain_prefix(&term->inbuf);
+                chars = data.ptr;
+                nchars_got = data.len;
+                assert(chars != NULL);
+                assert(nchars_used < nchars_got);
+            }
+            c = chars[nchars_used++];
+
+            /*
+             * Optionally log the session traffic to a file. Useful for
+             * debugging and possibly also useful for actual logging.
+             */
+            if (term->logtype == LGTYP_DEBUG && term->logctx)
+                logtraffic(term->logctx, (unsigned char) c, LGTYP_DEBUG);
+        }
+
+        /* Note only VT220+ are 8-bit VT102 is seven bit, it shouldn't even
+         * be able to display 8-bit characters, but I'll let that go 'cause
+         * of i18n.
+         */
+
+        /*
+         * If we're printing, add the character to the printer
+         * buffer.
+         */
+        if (term->printing) {
+            bufchain_add(&term->printer_buf, &c, 1);
+
+            /*
+             * If we're in print-only mode, we use a much simpler
+             * state machine designed only to recognise the ESC[4i
+             * termination sequence.
+             */
+            if (term->only_printing) {
+                if (c == '\033')
+                    term->print_state = 1;
+                else if (c == (unsigned char)'\233')
+                    term->print_state = 2;
+                else if (c == '[' && term->print_state == 1)
+                    term->print_state = 2;
+                else if (c == '4' && term->print_state == 2)
+                    term->print_state = 3;
+                else if (c == 'i' && term->print_state == 3)
+                    term->print_state = 4;
+                else
+                    term->print_state = 0;
+                if (term->print_state == 4) {
+                    term_print_finish(term);
+                }
+                continue;
+            }
+        }
+
+        /* Do character-set translation. */
+        if (term->termstate == TOPLEVEL) {
+            unsigned long t = term_translate(term, &term->utf8, c);
+            switch (t) {
+              case UCSINCOMPLETE:
+                continue;       /* didn't complete a multibyte char */
+              case UCSTRUNCATED:
+                unget = c;
+                /* fall through */
+              case UCSINVALID:
+                c = UCSERR;
+                break;
+              default:
+                c = t;
+                break;
+            }
+        }
+
+        /*
+         * How about C1 controls?
+         * Explicitly ignore SCI (0x9a), which we don't translate to DECID.
+         */
+        if ((c & -32) == 0x80 && term->termstate < DO_CTRLS &&
+            !term->vt52_mode && has_compat(VT220)) {
+            if (c == 0x9a)
+                c = 0;
+            else {
+                term->termstate = SEEN_ESC;
+                term->esc_query = 0;
+                c = '@' + (c & 0x1F);
+            }
+        }
+
+        /* Or the GL control. */
+        if (c == '\177' && term->termstate < DO_CTRLS && has_compat(OTHER)) {
+            if (term->curs.x && !term->wrapnext)
+                term->curs.x--;
+            term->wrapnext = false;
+            /* destructive backspace might be disabled */
+            if (!term->no_dbackspace) {
+                check_boundary(term, term->curs.x, term->curs.y);
+                check_boundary(term, term->curs.x+1, term->curs.y);
+                copy_termchar(scrlineptr(term->curs.y),
+                              term->curs.x, &term->erase_char);
+            }
+            seen_disp_event(term);
+        } else
+        /* Or normal C0 controls. */
+        if ((c & ~0x1F) == 0 && term->termstate < DO_CTRLS) {
+            switch (c) {
+              case '\005':             /* ENQ: terminal type query */
+                /*
+                 * Strictly speaking this is VT100 but a VT100 defaults to
+                 * no response. Other terminals respond at their option.
+                 *
+                 * Don't put a CR in the default string as this tends to
+                 * upset some weird software.
+                 */
+                compatibility(ANSIMIN);
+                if (term->ldisc) {
+                    strbuf *buf = term_input_data_from_charset(
+                        term, DEFAULT_CODEPAGE,
+                        term->answerback, term->answerbacklen);
+                    ldisc_send(term->ldisc, buf->s, buf->len, false);
+                    strbuf_free(buf);
+                }
+                break;
+              case '\007': {            /* BEL: Bell */
+                if (term->termstate == SEEN_OSC ||
+                    term->termstate == SEEN_OSC_W) {
+                    /*
+                     * In an OSC context, BEL is one of the ways to terminate
+                     * the whole sequence. We process it as such even if we
+                     * haven't got into the final OSC_STRING state yet, so that
+                     * OSC sequences without a string will be handled cleanly.
+                     */
+                    do_osc(term);
+                    term->termstate = TOPLEVEL;
+                    break;
+                }
+
+                struct beeptime *newbeep;
+                unsigned long ticks;
+
+                ticks = GETTICKCOUNT();
+
+                if (!term->beep_overloaded) {
+                    newbeep = snew(struct beeptime);
+                    newbeep->ticks = ticks;
+                    newbeep->next = NULL;
+                    if (!term->beephead)
+                        term->beephead = newbeep;
+                    else
+                        term->beeptail->next = newbeep;
+                    term->beeptail = newbeep;
+                    term->nbeeps++;
+                }
+
+                /*
+                 * Throw out any beeps that happened more than
+                 * t seconds ago.
+                 */
+                while (term->beephead &&
+                       term->beephead->ticks < ticks - term->bellovl_t) {
+                    struct beeptime *tmp = term->beephead;
+                    term->beephead = tmp->next;
+                    sfree(tmp);
+                    if (!term->beephead)
+                        term->beeptail = NULL;
+                    term->nbeeps--;
+                }
+
+                if (term->bellovl && term->beep_overloaded &&
+                    ticks - term->lastbeep >= (unsigned)term->bellovl_s) {
+                    /*
+                     * If we're currently overloaded and the
+                     * last beep was more than s seconds ago,
+                     * leave overload mode.
+                     */
+                    term->beep_overloaded = false;
+                } else if (term->bellovl && !term->beep_overloaded &&
+                           term->nbeeps >= term->bellovl_n) {
+                    /*
+                     * Now, if we have n or more beeps
+                     * remaining in the queue, go into overload
+                     * mode.
+                     */
+                    term->beep_overloaded = true;
+                }
+                term->lastbeep = ticks;
+
+                /*
+                 * Perform an actual beep if we're not overloaded.
+                 */
+                if (!term->bellovl || !term->beep_overloaded) {
+                    win_bell(term->win, term->beep);
+
+                    if (term->beep == BELL_VISUAL) {
+                        term_schedule_vbell(term, false, 0);
+                    }
+                }
+                seen_disp_event(term);
+                break;
+              }
+              case '\b':              /* BS: Back space */
+                if (term->wrapnext) {
+                    term->wrapnext = false;
+                } else if (term->curs.x == 0 &&
+                           (term->curs.y == 0 || !term->wrap)) {
+                    /* do nothing */
+                } else if (term->curs.x == 0 && term->curs.y > 0) {
+                    term->curs.x = term->cols - 1, term->curs.y--;
+
+                    /*
+                     * If the line we've just wrapped back on to had the
+                     * LATTR_WRAPPED2 flag set, it means that the line wrapped
+                     * because a double-width character was printed with the
+                     * cursor in the rightmost column, and the best handling
+                     * available was to leave that column empty and move the
+                     * whole character to the next line. In that situation,
+                     * backspacing needs to put the cursor on the previous
+                     * _logical_ character, i.e. skip the empty space left by
+                     * the wrapping. This arranges that if an application
+                     * unaware of the terminal width or cursor position prints
+                     * a number of printing characters and then tries to return
+                     * to a particular one of them by emitting the right number
+                     * of backspaces, it's still the right number even if a
+                     * line break appeared in a maximally awkward position.
+                     */
+                    termline *ldata = scrlineptr(term->curs.y);
+                    if (term->curs.x > 0 && (ldata->lattr & LATTR_WRAPPED2))
+                        term->curs.x--;
+                } else {
+                    term->curs.x--;
+                }
+                seen_disp_event(term);
+                break;
+              case '\016':            /* LS1: Locking-shift one */
+                compatibility(VT100);
+                term->cset = 1;
+                break;
+              case '\017':            /* LS0: Locking-shift zero */
+                compatibility(VT100);
+                term->cset = 0;
+                break;
+              case '\033':            /* ESC: Escape */
+                if (term->vt52_mode)
+                    term->termstate = VT52_ESC;
+                else if (term->termstate == SEEN_OSC ||
+                         term->termstate == SEEN_OSC_W) {
+                    /* Be prepared to terminate an OSC early */
+                    term->termstate = OSC_MAYBE_ST;
+                } else {
+                    compatibility(ANSIMIN);
+                    term->termstate = SEEN_ESC;
+                    term->esc_query = 0;
+                }
+                break;
+              case '\015':            /* CR: Carriage return */
+                term->curs.x = 0;
+                term->wrapnext = false;
+                seen_disp_event(term);
+
+                if (term->crhaslf) {
+                    if (term->curs.y == term->marg_b)
+                        scroll(term, term->marg_t, term->marg_b, 1, true);
+                    else if (term->curs.y < term->rows - 1)
+                        term->curs.y++;
+                }
+                if (term->logctx)
+                    logtraffic(term->logctx, (unsigned char) c, LGTYP_ASCII);
+                break;
+              case '\014':            /* FF: Form feed */
+                if (has_compat(SCOANSI)) {
+                    move(term, 0, 0, 0);
+                    erase_lots(term, false, false, true);
+                    if (term->scroll_on_disp)
+                        term->disptop = 0;
+                    term->wrapnext = false;
+                    seen_disp_event(term);
+                    break;
+                }
+              case '\013':            /* VT: Line tabulation */
+                compatibility(VT100);
+              case '\012':            /* LF: Line feed */
+                if (term->curs.y == term->marg_b)
+                    scroll(term, term->marg_t, term->marg_b, 1, true);
+                else if (term->curs.y < term->rows - 1)
+                    term->curs.y++;
+                if (term->lfhascr)
+                    term->curs.x = 0;
+                term->wrapnext = false;
+                seen_disp_event(term);
+                if (term->logctx)
+                    logtraffic(term->logctx, (unsigned char) c, LGTYP_ASCII);
+                break;
+              case '\t': {              /* HT: Character tabulation */
+                pos old_curs = term->curs;
+                termline *ldata = scrlineptr(term->curs.y);
+
+                do {
+                    term->curs.x++;
+                } while (term->curs.x < term->cols - 1 &&
+                         !term->tabs[term->curs.x]);
+
+                if ((ldata->lattr & LATTR_MODE) != LATTR_NORM) {
+                    if (term->curs.x >= term->cols / 2)
+                        term->curs.x = term->cols / 2 - 1;
+                } else {
+                    if (term->curs.x >= term->cols)
+                        term->curs.x = term->cols - 1;
+                }
+
+                check_selection(term, old_curs, term->curs);
+                seen_disp_event(term);
+                break;
+              }
+            }
+        } else
+            switch (term->termstate) {
+              case TOPLEVEL:
+                /* Only graphic characters get this far;
+                 * ctrls are stripped above */
+                term_display_graphic_char(term, c);
+                term->last_graphic_char = c;
+                break;
+
+              case OSC_MAYBE_ST:
+                /*
+                 * This state is virtually identical to SEEN_ESC, with the
+                 * exception that we have an OSC sequence in the pipeline,
+                 * and _if_ we see a backslash, we process it.
+                 */
+                if (c == '\\') {
+                    do_osc(term);
+                    term->termstate = TOPLEVEL;
+                    break;
+                }
+                /* else fall through */
+              case SEEN_ESC:
+                if (c >= ' ' && c <= '/') {
+                    if (term->esc_query)
+                        term->esc_query = -1;
+                    else
+                        term->esc_query = c;
+                    break;
+                }
+                term->termstate = TOPLEVEL;
+                switch (ANSI(c, term->esc_query)) {
+                  case '[':             /* enter CSI mode */
+                    term->termstate = SEEN_CSI;
+                    term->esc_nargs = 1;
+                    term->esc_args[0] = ARG_DEFAULT;
+                    term->esc_query = 0;
+                    break;
+                  case ']':             /* OSC: xterm escape sequences */
+                    /* Compatibility is nasty here, xterm, linux, decterm yuk! */
+                    compatibility(OTHER);
+                    term->termstate = SEEN_OSC;
+                    term->osc_is_apc = false;
+                    term->osc_strlen = 0;
+                    term->esc_args[0] = 0;
+                    term->esc_nargs = 1;
+                    break;
+                  case '_':             /* APC: application program command */
+                    /* APC sequences are just a string, terminated by
+                     * ST or (I've observed in practice) ^G. That is,
+                     * they have the same termination convention as
+                     * OSC. So we handle them by going straight into
+                     * OSC_STRING state and setting a flag indicating
+                     * that it's not really an OSC. */
+                    compatibility(OTHER);
+                    term->termstate = SEEN_OSC;
+                    term->osc_is_apc = true;
+                    term->osc_strlen = 0;
+                    term->esc_args[0] = 0;
+                    term->esc_nargs = 1;
+                    break;
+                  case '7':             /* DECSC: save cursor */
+                    compatibility(VT100);
+                    save_cursor(term, true);
+                    break;
+                  case '8':             /* DECRC: restore cursor */
+                    compatibility(VT100);
+                    save_cursor(term, false);
+                    break;
+                  case '=':             /* DECKPAM: Keypad application mode */
+                    compatibility(VT100);
+                    term->app_keypad_keys = true;
+                    break;
+                  case '>':             /* DECKPNM: Keypad numeric mode */
+                    compatibility(VT100);
+                    term->app_keypad_keys = false;
+                    break;
+                  case 'D':            /* IND: exactly equivalent to LF */
+                    compatibility(VT100);
+                    if (term->curs.y == term->marg_b)
+                        scroll(term, term->marg_t, term->marg_b, 1, true);
+                    else if (term->curs.y < term->rows - 1)
+                        term->curs.y++;
+                    term->wrapnext = false;
+                    seen_disp_event(term);
+                    break;
+                  case 'E':            /* NEL: exactly equivalent to CR-LF */
+                    compatibility(VT100);
+                    term->curs.x = 0;
+                    if (term->curs.y == term->marg_b)
+                        scroll(term, term->marg_t, term->marg_b, 1, true);
+                    else if (term->curs.y < term->rows - 1)
+                        term->curs.y++;
+                    term->wrapnext = false;
+                    seen_disp_event(term);
+                    break;
+                  case 'M':            /* RI: reverse index - backwards LF */
+                    compatibility(VT100);
+                    if (term->curs.y == term->marg_t)
+                        scroll(term, term->marg_t, term->marg_b, -1, true);
+                    else if (term->curs.y > 0)
+                        term->curs.y--;
+                    term->wrapnext = false;
+                    seen_disp_event(term);
+                    break;
+                  case 'Z':            /* DECID: terminal type query */
+                    compatibility(VT100);
+                    if (term->ldisc)
+                        ldisc_send(term->ldisc, term->id_string,
+                                   strlen(term->id_string), false);
+                    break;
+                  case 'c':            /* RIS: restore power-on settings */
+                    compatibility(VT100);
+                    power_on(term, true);
+                    if (term->ldisc)   /* cause ldisc to notice changes */
+                        ldisc_echoedit_update(term->ldisc);
+                    if (term->reset_132) {
+                        if (!term->no_remote_resize)
+                            term_request_resize(term, 80, term->rows);
+                        term->reset_132 = false;
+                    }
+                    if (term->scroll_on_disp)
+                        term->disptop = 0;
+                    seen_disp_event(term);
+                    break;
+                  case 'H':            /* HTS: set a tab */
+                    compatibility(VT100);
+                    term->tabs[term->curs.x] = true;
+                    break;
+
+                  case ANSI('8', '#'): { /* DECALN: fills screen with Es :-) */
+                    compatibility(VT100);
+                    termline *ldata;
+                    int i, j;
+                    pos scrtop, scrbot;
+
+                    for (i = 0; i < term->rows; i++) {
+                        ldata = scrlineptr(i);
+                        check_line_size(term, ldata);
+                        for (j = 0; j < term->cols; j++) {
+                            copy_termchar(ldata, j,
+                                          &term->basic_erase_char);
+                            ldata->chars[j].chr = 'E';
+                        }
+                        ldata->lattr = LATTR_NORM;
+                    }
+                    if (term->scroll_on_disp)
+                        term->disptop = 0;
+                    seen_disp_event(term);
+                    scrtop.x = scrtop.y = 0;
+                    scrbot.x = 0;
+                    scrbot.y = term->rows;
+                    check_selection(term, scrtop, scrbot);
+                    break;
+                  }
+
+                  case ANSI('3', '#'):
+                  case ANSI('4', '#'):
+                  case ANSI('5', '#'):
+                  case ANSI('6', '#'): {
+                    compatibility(VT100);
+                    int nlattr;
+                    termline *ldata;
+
+                    switch (ANSI(c, term->esc_query)) {
+                      case ANSI('3', '#'): /* DECDHL: 2*height, top */
+                        nlattr = LATTR_TOP;
+                        break;
+                      case ANSI('4', '#'): /* DECDHL: 2*height, bottom */
+                        nlattr = LATTR_BOT;
+                        break;
+                      case ANSI('5', '#'): /* DECSWL: normal */
+                        nlattr = LATTR_NORM;
+                        break;
+                      default: /* case ANSI('6', '#'): DECDWL: 2*width */
+                        nlattr = LATTR_WIDE;
+                        break;
+                    }
+                    ldata = scrlineptr(term->curs.y);
+                    check_line_size(term, ldata);
+                    check_trust_status(term, ldata);
+                    ldata->lattr = nlattr;
+                    seen_disp_event(term);
+                    break;
+                  }
+                  /* GZD4: G0 designate 94-set */
+                  case ANSI('A', '('):
+                    compatibility(VT100);
+                    if (!term->no_remote_charset)
+                        term->cset_attr[0] = CSET_GBCHR;
+                    break;
+                  case ANSI('B', '('):
+                    compatibility(VT100);
+                    if (!term->no_remote_charset)
+                        term->cset_attr[0] = CSET_ASCII;
+                    break;
+                  case ANSI('0', '('):
+                    compatibility(VT100);
+                    if (!term->no_remote_charset)
+                        term->cset_attr[0] = CSET_LINEDRW;
+                    break;
+                  case ANSI('U', '('):
+                    compatibility(OTHER);
+                    if (!term->no_remote_charset)
+                        term->cset_attr[0] = CSET_SCOACS;
+                    break;
+                  /* G1D4: G1-designate 94-set */
+                  case ANSI('A', ')'):
+                    compatibility(VT100);
+                    if (!term->no_remote_charset)
+                        term->cset_attr[1] = CSET_GBCHR;
+                    break;
+                  case ANSI('B', ')'):
+                    compatibility(VT100);
+                    if (!term->no_remote_charset)
+                        term->cset_attr[1] = CSET_ASCII;
+                    break;
+                  case ANSI('0', ')'):
+                    compatibility(VT100);
+                    if (!term->no_remote_charset)
+                        term->cset_attr[1] = CSET_LINEDRW;
+                    break;
+                  case ANSI('U', ')'):
+                    compatibility(OTHER);
+                    if (!term->no_remote_charset)
+                        term->cset_attr[1] = CSET_SCOACS;
+                    break;
+                  /* DOCS: Designate other coding system */
+                  case ANSI('8', '%'):  /* Old Linux code */
+                  case ANSI('G', '%'):
+                    compatibility(OTHER);
+                    if (!term->no_remote_charset)
+                        term->utf = true;
+                    break;
+                  case ANSI('@', '%'):
+                    compatibility(OTHER);
+                    if (!term->no_remote_charset)
+                        term->utf = false;
+                    break;
+                }
+                break;
+              case SEEN_CSI:
+                term->termstate = TOPLEVEL;  /* default */
+                if (isdigit(c)) {
+                    if (term->esc_nargs <= ARGS_MAX) {
+                        if (term->esc_args[term->esc_nargs - 1] == ARG_DEFAULT)
+                            term->esc_args[term->esc_nargs - 1] = 0;
+                        if (term->esc_args[term->esc_nargs - 1] <=
+                            UINT_MAX / 10 &&
+                            term->esc_args[term->esc_nargs - 1] * 10 <=
+                            UINT_MAX - c - '0')
+                            term->esc_args[term->esc_nargs - 1] =
+                                10 * term->esc_args[term->esc_nargs - 1] +
+                                c - '0';
+                        else
+                            term->esc_args[term->esc_nargs - 1] = UINT_MAX;
+                    }
+                    term->termstate = SEEN_CSI;
+                } else if (c == ';') {
+                    if (term->esc_nargs < ARGS_MAX)
+                        term->esc_args[term->esc_nargs++] = ARG_DEFAULT;
+                    term->termstate = SEEN_CSI;
+                } else if (c < '@') {
+                    if (term->esc_query)
+                        term->esc_query = -1;
+                    else if (c == '?')
+                        term->esc_query = 1;
+                    else
+                        term->esc_query = c;
+                    term->termstate = SEEN_CSI;
+                } else
+#define CLAMP(arg, lim) ((arg) = ((arg) > (lim)) ? (lim) : (arg))
+                    switch (ANSI(c, term->esc_query)) {
+                      case 'A':       /* CUU: move up N lines */
+                        CLAMP(term->esc_args[0], term->rows);
+                        move(term, term->curs.x,
+                             term->curs.y - def(term->esc_args[0], 1), 1);
+                        seen_disp_event(term);
+                        break;
+                      case 'e':         /* VPR: move down N lines */
+                        compatibility(ANSI);
+                        /* FALLTHROUGH */
+                      case 'B':         /* CUD: Cursor down */
+                        CLAMP(term->esc_args[0], term->rows);
+                        move(term, term->curs.x,
+                             term->curs.y + def(term->esc_args[0], 1), 1);
+                        seen_disp_event(term);
+                        break;
+                      case 'b':        /* REP: repeat previous grap */
+                        CLAMP(term->esc_args[0], term->rows * term->cols);
+                        if (term->last_graphic_char) {
+                            unsigned i;
+                            for (i = 0; i < term->esc_args[0]; i++)
+                                term_display_graphic_char(
+                                    term, term->last_graphic_char);
+                        }
+                        break;
+                      case ANSI('c', '>'):      /* DA: report xterm version */
+                        compatibility(OTHER);
+                        /* this reports xterm version 136 so that VIM can
+                           use the drag messages from the mouse reporting */
+                        if (term->ldisc)
+                            ldisc_send(term->ldisc, "\033[>0;136;0c", 11,
+                                       false);
+                        break;
+                      case 'a':         /* HPR: move right N cols */
+                        compatibility(ANSI);
+                        /* FALLTHROUGH */
+                      case 'C':         /* CUF: Cursor right */
+                        CLAMP(term->esc_args[0], term->cols);
+                        move(term, term->curs.x + def(term->esc_args[0], 1),
+                             term->curs.y, 1);
+                        seen_disp_event(term);
+                        break;
+                      case 'D':       /* CUB: move left N cols */
+                        CLAMP(term->esc_args[0], term->cols);
+                        move(term, term->curs.x - def(term->esc_args[0], 1),
+                             term->curs.y, 1);
+                        seen_disp_event(term);
+                        break;
+                      case 'E':       /* CNL: move down N lines and CR */
+                        compatibility(ANSI);
+                        CLAMP(term->esc_args[0], term->rows);
+                        move(term, 0,
+                             term->curs.y + def(term->esc_args[0], 1), 1);
+                        seen_disp_event(term);
+                        break;
+                      case 'F':       /* CPL: move up N lines and CR */
+                        compatibility(ANSI);
+                        CLAMP(term->esc_args[0], term->rows);
+                        move(term, 0,
+                             term->curs.y - def(term->esc_args[0], 1), 1);
+                        seen_disp_event(term);
+                        break;
+                      case 'G':       /* CHA */
+                      case '`':       /* HPA: set horizontal posn */
+                        compatibility(ANSI);
+                        CLAMP(term->esc_args[0], term->cols);
+                        move(term, def(term->esc_args[0], 1) - 1,
+                             term->curs.y, 0);
+                        seen_disp_event(term);
+                        break;
+                      case 'd':       /* VPA: set vertical posn */
+                        compatibility(ANSI);
+                        CLAMP(term->esc_args[0], term->rows);
+                        move(term, term->curs.x,
+                             ((term->dec_om ? term->marg_t : 0) +
+                              def(term->esc_args[0], 1) - 1),
+                             (term->dec_om ? 2 : 0));
+                        seen_disp_event(term);
+                        break;
+                      case 'H':      /* CUP */
+                      case 'f':      /* HVP: set horz and vert posns at once */
+                        if (term->esc_nargs < 2)
+                            term->esc_args[1] = ARG_DEFAULT;
+                        CLAMP(term->esc_args[0], term->rows);
+                        CLAMP(term->esc_args[1], term->cols);
+                        move(term, def(term->esc_args[1], 1) - 1,
+                             ((term->dec_om ? term->marg_t : 0) +
+                              def(term->esc_args[0], 1) - 1),
+                             (term->dec_om ? 2 : 0));
+                        seen_disp_event(term);
+                        break;
+                      case 'J': {       /* ED: erase screen or parts of it */
+                        unsigned int i = def(term->esc_args[0], 0);
+                        if (i == 3) {
+                            /* Erase Saved Lines (xterm)
+                             * This follows Thomas Dickey's xterm. */
+                            if (!term->no_remote_clearscroll)
+                                term_clrsb(term);
+                        } else {
+                            i++;
+                            if (i > 3)
+                                i = 0;
+                            erase_lots(term, false, !!(i & 2), !!(i & 1));
+                        }
+                        if (term->scroll_on_disp)
+                            term->disptop = 0;
+                        seen_disp_event(term);
+                        break;
+                      }
+                      case 'K': {       /* EL: erase line or parts of it */
+                        unsigned int i = def(term->esc_args[0], 0) + 1;
+                        if (i > 3)
+                            i = 0;
+                        erase_lots(term, true, !!(i & 2), !!(i & 1));
+                        seen_disp_event(term);
+                        break;
+                      }
+                      case 'L':       /* IL: insert lines */
+                        compatibility(VT102);
+                        CLAMP(term->esc_args[0], term->rows);
+                        if (term->curs.y <= term->marg_b)
+                            scroll(term, term->curs.y, term->marg_b,
+                                   -def(term->esc_args[0], 1), false);
+                        seen_disp_event(term);
+                        break;
+                      case 'M':       /* DL: delete lines */
+                        compatibility(VT102);
+                        CLAMP(term->esc_args[0], term->rows);
+                        if (term->curs.y <= term->marg_b)
+                            scroll(term, term->curs.y, term->marg_b,
+                                   def(term->esc_args[0], 1),
+                                   true);
+                        seen_disp_event(term);
+                        break;
+                      case '@':       /* ICH: insert chars */
+                        /* XXX VTTEST says this is vt220, vt510 manual says vt102 */
+                        compatibility(VT102);
+                        CLAMP(term->esc_args[0], term->cols);
+                        insch(term, def(term->esc_args[0], 1));
+                        seen_disp_event(term);
+                        break;
+                      case 'P':       /* DCH: delete chars */
+                        compatibility(VT102);
+                        CLAMP(term->esc_args[0], term->cols);
+                        insch(term, -def(term->esc_args[0], 1));
+                        seen_disp_event(term);
+                        break;
+                      case 'c':       /* DA: terminal type query */
+                        compatibility(VT100);
+                        /* This is the response for a VT102 */
+                        if (term->ldisc)
+                            ldisc_send(term->ldisc, term->id_string,
+                                       strlen(term->id_string), false);
+                        break;
+                      case 'n':       /* DSR: cursor position query */
+                        if (term->ldisc) {
+                            if (term->esc_args[0] == 6) {
+                                char buf[32];
+                                sprintf(buf, "\033[%d;%dR", term->curs.y + 1,
+                                        term->curs.x + 1);
+                                ldisc_send(term->ldisc, buf, strlen(buf),
+                                           false);
+                            } else if (term->esc_args[0] == 5) {
+                                ldisc_send(term->ldisc, "\033[0n", 4, false);
+                            }
+                        }
+                        break;
+                      case 'h':       /* SM: toggle modes to high */
+                      case ANSI_QUE('h'):
+                        compatibility(VT100);
+                        for (int i = 0; i < term->esc_nargs; i++)
+                            toggle_mode(term, term->esc_args[i],
+                                        term->esc_query, true);
+                        break;
+                      case 'i':         /* MC: Media copy */
+                      case ANSI_QUE('i'): {
+                        compatibility(VT100);
+                        char *printer;
+                        if (term->esc_nargs != 1) break;
+                        if (term->esc_args[0] == 5 &&
+                            (printer = conf_get_str(term->conf,
+                                                    CONF_printer))[0]) {
+                            term->printing = true;
+                            term->only_printing = !term->esc_query;
+                            term->print_state = 0;
+                            term_print_setup(term, printer);
+                        } else if (term->esc_args[0] == 4 &&
+                                   term->printing) {
+                            term_print_finish(term);
+                        }
+                        break;
+                      }
+                      case 'l':       /* RM: toggle modes to low */
+                      case ANSI_QUE('l'):
+                        compatibility(VT100);
+                        for (int i = 0; i < term->esc_nargs; i++)
+                            toggle_mode(term, term->esc_args[i],
+                                        term->esc_query, false);
+                        break;
+                      case 'g':       /* TBC: clear tabs */
+                        compatibility(VT100);
+                        if (term->esc_nargs == 1) {
+                            if (term->esc_args[0] == 0) {
+                                term->tabs[term->curs.x] = false;
+                            } else if (term->esc_args[0] == 3) {
+                                int i;
+                                for (i = 0; i < term->cols; i++)
+                                    term->tabs[i] = false;
+                            }
+                        }
+                        break;
+                      case 'r':       /* DECSTBM: set scroll margins */
+                        compatibility(VT100);
+                        if (term->esc_nargs <= 2) {
+                            int top, bot;
+                            CLAMP(term->esc_args[0], term->rows);
+                            CLAMP(term->esc_args[1], term->rows);
+                            top = def(term->esc_args[0], 1) - 1;
+                            bot = (term->esc_nargs <= 1
+                                   || term->esc_args[1] == 0 ?
+                                   term->rows :
+                                   def(term->esc_args[1], term->rows)) - 1;
+                            if (bot >= term->rows)
+                                bot = term->rows - 1;
+                            /* VTTEST Bug 9 - if region is less than 2 lines
+                             * don't change region.
+                             */
+                            if (bot - top > 0) {
+                                term->marg_t = top;
+                                term->marg_b = bot;
+                                term->curs.x = 0;
+                                /*
+                                 * I used to think the cursor should be
+                                 * placed at the top of the newly marginned
+                                 * area. Apparently not: VMS TPU falls over
+                                 * if so.
+                                 *
+                                 * Well actually it should for
+                                 * Origin mode - RDB
+                                 */
+                                term->curs.y = (term->dec_om ?
+                                                term->marg_t : 0);
+                                seen_disp_event(term);
+                            }
+                        }
+                        break;
+                      case 'm':       /* SGR: set graphics rendition */
+                        /*
+                         * 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
+                         * 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.
+                         */
+                        for (int i = 0; i < term->esc_nargs; i++)
+                        switch (def(term->esc_args[i], 0)) {
+                          case 0:       /* restore defaults */
+                            term->curr_attr = term->default_attr;
+                            term->curr_truecolour =
+                                term->basic_erase_char.truecolour;
+                            break;
+                          case 1:       /* enable bold */
+                            compatibility(VT100AVO);
+                            term->curr_attr |= ATTR_BOLD;
+                            break;
+                          case 2:       /* enable dim */
+                            compatibility(OTHER);
+                            term->curr_attr |= ATTR_DIM;
+                            break;
+                          case 21:      /* (enable double underline) */
+                            compatibility(OTHER);
+                          case 4:       /* enable underline */
+                            compatibility(VT100AVO);
+                            term->curr_attr |= ATTR_UNDER;
+                            break;
+                          case 5:       /* enable blink */
+                            compatibility(VT100AVO);
+                            term->curr_attr |= ATTR_BLINK;
+                            break;
+                          case 6:       /* SCO light bkgrd */
+                            compatibility(SCOANSI);
+                            term->blink_is_real = false;
+                            term->curr_attr |= ATTR_BLINK;
+                            term_schedule_tblink(term);
+                            break;
+                          case 7:       /* enable reverse video */
+                            term->curr_attr |= ATTR_REVERSE;
+                            break;
+                          case 9:       /* enable strikethrough */
+                            term->curr_attr |= ATTR_STRIKE;
+                            break;
+                          case 10:      /* SCO acs off */
+                            compatibility(SCOANSI);
+                            if (term->no_remote_charset) break;
+                            term->sco_acs = 0; break;
+                          case 11:      /* SCO acs on */
+                            compatibility(SCOANSI);
+                            if (term->no_remote_charset) break;
+                            term->sco_acs = 1; break;
+                          case 12:      /* SCO acs on, |0x80 */
+                            compatibility(SCOANSI);
+                            if (term->no_remote_charset) break;
+                            term->sco_acs = 2; break;
+                          case 22:      /* disable bold and dim */
+                            compatibility2(OTHER, VT220);
+                            term->curr_attr &= ~(ATTR_BOLD | ATTR_DIM);
+                            break;
+                          case 24:      /* disable underline */
+                            compatibility2(OTHER, VT220);
+                            term->curr_attr &= ~ATTR_UNDER;
+                            break;
+                          case 25:      /* disable blink */
+                            compatibility2(OTHER, VT220);
+                            term->curr_attr &= ~ATTR_BLINK;
+                            break;
+                          case 27:      /* disable reverse video */
+                            compatibility2(OTHER, VT220);
+                            term->curr_attr &= ~ATTR_REVERSE;
+                            break;
+                          case 29:      /* disable strikethrough */
+                            term->curr_attr &= ~ATTR_STRIKE;
+                            break;
+                          case 30:
+                          case 31:
+                          case 32:
+                          case 33:
+                          case 34:
+                          case 35:
+                          case 36:
+                          case 37:
+                            /* foreground */
+                            term->curr_truecolour.fg.enabled = false;
+                            term->curr_attr &= ~ATTR_FGMASK;
+                            term->curr_attr |=
+                                (term->esc_args[i] - 30)<<ATTR_FGSHIFT;
+                            break;
+                          case 90:
+                          case 91:
+                          case 92:
+                          case 93:
+                          case 94:
+                          case 95:
+                          case 96:
+                          case 97:
+                            /* aixterm-style bright foreground */
+                            term->curr_truecolour.fg.enabled = false;
+                            term->curr_attr &= ~ATTR_FGMASK;
+                            term->curr_attr |=
+                                ((term->esc_args[i] - 90 + 8)
+                                 << ATTR_FGSHIFT);
+                            break;
+                          case 39:      /* default-foreground */
+                            term->curr_truecolour.fg.enabled = false;
+                            term->curr_attr &= ~ATTR_FGMASK;
+                            term->curr_attr |= ATTR_DEFFG;
+                            break;
+                          case 40:
+                          case 41:
+                          case 42:
+                          case 43:
+                          case 44:
+                          case 45:
+                          case 46:
+                          case 47:
+                            /* background */
+                            term->curr_truecolour.bg.enabled = false;
+                            term->curr_attr &= ~ATTR_BGMASK;
+                            term->curr_attr |=
+                                (term->esc_args[i] - 40)<<ATTR_BGSHIFT;
+                            break;
+                          case 100:
+                          case 101:
+                          case 102:
+                          case 103:
+                          case 104:
+                          case 105:
+                          case 106:
+                          case 107:
+                            /* aixterm-style bright background */
+                            term->curr_truecolour.bg.enabled = false;
+                            term->curr_attr &= ~ATTR_BGMASK;
+                            term->curr_attr |=
+                                ((term->esc_args[i] - 100 + 8)
+                                 << ATTR_BGSHIFT);
+                            break;
+                          case 49:      /* default-background */
+                            term->curr_truecolour.bg.enabled = false;
+                            term->curr_attr &= ~ATTR_BGMASK;
+                            term->curr_attr |= ATTR_DEFBG;
+                            break;
+
+                            /*
+                             * 256-colour and true-colour
+                             * sequences. A 256-colour
+                             * foreground is selected by a
+                             * sequence of 3 arguments in the
+                             * form 38;5;n, where n is in the
+                             * range 0-255. A true-colour RGB
+                             * triple is selected by 5 args of
+                             * the form 38;2;r;g;b. Replacing
+                             * the initial 38 with 48 in both
+                             * cases selects the same colour
+                             * as the background.
+                             */
+                          case 38:
+                            if (i+2 < term->esc_nargs &&
+                                term->esc_args[i+1] == 5) {
+                                term->curr_attr &= ~ATTR_FGMASK;
+                                term->curr_attr |=
+                                    ((term->esc_args[i+2] & 0xFF)
+                                     << ATTR_FGSHIFT);
+                                term->curr_truecolour.fg =
+                                    optionalrgb_none;
+                                i += 2;
+                            }
+                            if (i + 4 < term->esc_nargs &&
+                                term->esc_args[i + 1] == 2) {
+                                parse_optionalrgb(
+                                    &term->curr_truecolour.fg,
+                                    term->esc_args + (i+2));
+                                i += 4;
+                            }
+                            break;
+                          case 48:
+                            if (i+2 < term->esc_nargs &&
+                                term->esc_args[i+1] == 5) {
+                                term->curr_attr &= ~ATTR_BGMASK;
+                                term->curr_attr |=
+                                    ((term->esc_args[i+2] & 0xFF)
+                                     << ATTR_BGSHIFT);
+                                term->curr_truecolour.bg =
+                                    optionalrgb_none;
+                                i += 2;
+                            }
+                            if (i + 4 < term->esc_nargs &&
+                                term->esc_args[i+1] == 2) {
+                                parse_optionalrgb(
+                                    &term->curr_truecolour.bg,
+                                    term->esc_args + (i+2));
+                                i += 4;
+                            }
+                            break;
+                        }
+                        set_erase_char(term);
+                        break;
+                      case 's':       /* save cursor */
+                        save_cursor(term, true);
+                        break;
+                      case 'u':       /* restore cursor */
+                        save_cursor(term, false);
+                        break;
+                      case 't': /* DECSLPP: set page size - ie window height */
+                        /*
+                         * VT340/VT420 sequence DECSLPP, DEC only allows values
+                         *  24/25/36/48/72/144 other emulators (eg dtterm) use
+                         * illegal values (eg first arg 1..9) for window changing
+                         * and reports.
+                         */
+                        if (term->esc_nargs <= 1
+                            && (term->esc_args[0] < 1 ||
+                                term->esc_args[0] >= 24)) {
+                            compatibility(VT340TEXT);
+                            if (!term->no_remote_resize)
+                                term_request_resize(term, term->cols, 24);
+                            deselect(term);
+                        } else if (term->esc_nargs >= 1 &&
+                                   term->esc_args[0] >= 1 &&
+                                   term->esc_args[0] < 24) {
+                            compatibility(OTHER);
+
+                            switch (term->esc_args[0]) {
+                                int len;
+                                char buf[80];
+                                const char *p;
+                              case 1:
+                                term->win_minimise_pending = true;
+                                term->win_minimise_enable = false;
+                                term_schedule_update(term);
+                                break;
+                              case 2:
+                                term->win_minimise_pending = true;
+                                term->win_minimise_enable = true;
+                                term_schedule_update(term);
+                                break;
+                              case 3:
+                                if (term->esc_nargs >= 3) {
+                                    if (!term->no_remote_resize) {
+                                        term->win_move_pending = true;
+                                        term->win_move_pending_x =
+                                            def(term->esc_args[1], 0);
+                                        term->win_move_pending_y =
+                                            def(term->esc_args[2], 0);
+                                        term_schedule_update(term);
+                                    }
+                                }
+                                break;
+                              case 4:
+                                /* We should resize the window to a given
+                                 * size in pixels here, but currently our
+                                 * resizing code isn't healthy enough to
+                                 * manage it. */
+                                break;
+                              case 5:
+                                /* move to top */
+                                term->win_zorder_pending = true;
+                                term->win_zorder_top = true;
+                                term_schedule_update(term);
+                                break;
+                              case 6:
+                                /* move to bottom */
+                                term->win_zorder_pending = true;
+                                term->win_zorder_top = false;
+                                term_schedule_update(term);
+                                break;
+                              case 7:
+                                term->win_refresh_pending = true;
+                                term_schedule_update(term);
+                                break;
+                              case 8:
+                                if (term->esc_nargs >= 3 &&
+                                    !term->no_remote_resize) {
+                                    term_request_resize(
+                                        term,
+                                        def(term->esc_args[2],
+                                            term->conf_width),
+                                        def(term->esc_args[1],
+                                            term->conf_height));
+                                }
+                                break;
+                              case 9:
+                                if (term->esc_nargs >= 2) {
+                                    term->win_maximise_pending = true;
+                                    term->win_maximise_enable =
+                                        term->esc_args[1];
+                                    term_schedule_update(term);
+                                }
+                                break;
+                              case 11:
+                                if (term->ldisc)
+                                    ldisc_send(term->ldisc, term->minimised ?
+                                               "\033[2t" : "\033[1t", 4,
+                                               false);
+                                break;
+                              case 13:
+                                if (term->ldisc) {
+                                    len = sprintf(buf, "\033[3;%u;%ut",
+                                                  term->winpos_x,
+                                                  term->winpos_y);
+                                    ldisc_send(term->ldisc, buf, len, false);
+                                }
+                                break;
+                              case 14:
+                                if (term->ldisc) {
+                                    len = sprintf(buf, "\033[4;%u;%ut",
+                                                  term->winpixsize_y,
+                                                  term->winpixsize_x);
+                                    ldisc_send(term->ldisc, buf, len, false);
+                                }
+                                break;
+                              case 18:
+                                if (term->ldisc) {
+                                    len = sprintf(buf, "\033[8;%d;%dt",
+                                                  term->rows, term->cols);
+                                    ldisc_send(term->ldisc, buf, len, false);
+                                }
+                                break;
+                              case 19:
+                                /*
+                                 * Hmmm. Strictly speaking we
+                                 * should return `the size of the
+                                 * screen in characters', but
+                                 * that's not easy: (a) window
+                                 * furniture being what it is it's
+                                 * hard to compute, and (b) in
+                                 * resize-font mode maximising the
+                                 * window wouldn't change the
+                                 * number of characters. *shrug*. I
+                                 * think we'll ignore it for the
+                                 * moment and see if anyone
+                                 * complains, and then ask them
+                                 * what they would like it to do.
+                                 */
+                                break;
+                              case 20:
+                                if (term->ldisc &&
+                                    term->remote_qtitle_action != TITLE_NONE) {
+                                    if(term->remote_qtitle_action == TITLE_REAL)
+                                        p = term->icon_title;
+                                    else
+                                        p = EMPTY_WINDOW_TITLE;
+                                    len = strlen(p);
+                                    ldisc_send(term->ldisc, "\033]L", 3,
+                                               false);
+                                    ldisc_send(term->ldisc, p, len, false);
+                                    ldisc_send(term->ldisc, "\033\\", 2,
+                                               false);
+                                }
+                                break;
+                              case 21:
+                                if (term->ldisc &&
+                                    term->remote_qtitle_action != TITLE_NONE) {
+                                    if(term->remote_qtitle_action == TITLE_REAL)
+                                        p = term->window_title;
+                                    else
+                                        p = EMPTY_WINDOW_TITLE;
+                                    len = strlen(p);
+                                    ldisc_send(term->ldisc, "\033]l", 3,
+                                               false);
+                                    ldisc_send(term->ldisc, p, len, false);
+                                    ldisc_send(term->ldisc, "\033\\", 2,
+                                               false);
+                                }
+                                break;
+                            }
+                        }
+                        break;
+                      case 'S':         /* SU: Scroll up */
+                        CLAMP(term->esc_args[0], term->rows);
+                        compatibility(SCOANSI);
+                        scroll(term, term->marg_t, term->marg_b,
+                               def(term->esc_args[0], 1), true);
+                        term->wrapnext = false;
+                        break;
+                      case 'T':         /* SD: Scroll down */
+                        CLAMP(term->esc_args[0], term->rows);
+                        compatibility(SCOANSI);
+                        scroll(term, term->marg_t, term->marg_b,
+                               -def(term->esc_args[0], 1), true);
+                        term->wrapnext = false;
+                        break;
+                      case ANSI('|', '*'): /* DECSNLS */
+                        /*
+                         * Set number of lines on screen
+                         * VT420 uses VGA like hardware and can
+                         * support any size in reasonable range
+                         * (24..49 AIUI) with no default specified.
+                         */
+                        compatibility(VT420);
+                        if (term->esc_nargs == 1 && term->esc_args[0] > 0) {
+                            if (!term->no_remote_resize)
+                                term_request_resize(
+                                    term,
+                                    term->cols,
+                                    def(term->esc_args[0], term->conf_height));
+                            deselect(term);
+                        }
+                        break;
+                      case ANSI('|', '$'): /* DECSCPP */
+                        /*
+                         * Set number of columns per page
+                         * Docs imply range is only 80 or 132, but
+                         * I'll allow any.
+                         */
+                        compatibility(VT340TEXT);
+                        if (term->esc_nargs <= 1) {
+                            if (!term->no_remote_resize)
+                                term_request_resize(
+                                    term,
+                                    def(term->esc_args[0], term->conf_width),
+                                    term->rows);
+                            deselect(term);
+                        }
+                        break;
+                      case 'X': {   /* ECH: write N spaces w/o moving cursor */
+                        /* XXX VTTEST says this is vt220, vt510 manual
+                         * says vt100 */
+                        compatibility(ANSIMIN);
+                        CLAMP(term->esc_args[0], term->cols);
+                        int n = def(term->esc_args[0], 1);
+                        pos cursplus;
+                        int p = term->curs.x;
+                        termline *cline = scrlineptr(term->curs.y);
+
+                        check_trust_status(term, cline);
+                        if (n > term->cols - term->curs.x)
+                            n = term->cols - term->curs.x;
+                        cursplus = term->curs;
+                        cursplus.x += n;
+                        check_boundary(term, term->curs.x, term->curs.y);
+                        check_boundary(term, term->curs.x+n, term->curs.y);
+                        check_selection(term, term->curs, cursplus);
+                        while (n--)
+                            copy_termchar(cline, p++,
+                                          &term->erase_char);
+                        seen_disp_event(term);
+                        break;
+                      }
+                      case 'x':       /* DECREQTPARM: report terminal characteristics */
+                        compatibility(VT100);
+                        if (term->ldisc) {
+                            char buf[32];
+                            int i = def(term->esc_args[0], 0);
+                            if (i == 0 || i == 1) {
+                                strcpy(buf, "\033[2;1;1;112;112;1;0x");
+                                buf[2] += i;
+                                ldisc_send(term->ldisc, buf, 20, false);
+                            }
+                        }
+                        break;
+                      case 'Z': {         /* CBT */
+                        compatibility(OTHER);
+                        CLAMP(term->esc_args[0], term->cols);
+                        int i = def(term->esc_args[0], 1);
+                        pos old_curs = term->curs;
+
+                        for(;i>0 && term->curs.x>0; i--) {
+                            do {
+                                term->curs.x--;
+                            } while (term->curs.x >0 &&
+                                     !term->tabs[term->curs.x]);
+                        }
+                        check_selection(term, old_curs, term->curs);
+                        break;
+                      }
+                      case ANSI('c', '='):      /* Hide or Show Cursor */
+                        compatibility(SCOANSI);
+                        switch(term->esc_args[0]) {
+                          case 0:  /* hide cursor */
+                            term->cursor_on = false;
+                            break;
+                          case 1:  /* restore cursor */
+                            term->big_cursor = false;
+                            term->cursor_on = true;
+                            break;
+                          case 2:  /* block cursor */
+                            term->big_cursor = true;
+                            term->cursor_on = true;
+                            break;
+                        }
+                        break;
+                      case ANSI('C', '='):
+                        /*
+                         * set cursor start on scanline esc_args[0] and
+                         * end on scanline esc_args[1].If you set
+                         * the bottom scan line to a value less than
+                         * the top scan line, the cursor will disappear.
+                         */
+                        compatibility(SCOANSI);
+                        if (term->esc_nargs >= 2) {
+                            if (term->esc_args[0] > term->esc_args[1])
+                                term->cursor_on = false;
+                            else
+                                term->cursor_on = true;
+                        }
+                        break;
+                      case ANSI('D', '='):
+                        compatibility(SCOANSI);
+                        term->blink_is_real = false;
+                        term_schedule_tblink(term);
+                        if (term->esc_args[0]>=1)
+                            term->curr_attr |= ATTR_BLINK;
+                        else
+                            term->curr_attr &= ~ATTR_BLINK;
+                        break;
+                      case ANSI('E', '='):
+                        compatibility(SCOANSI);
+                        term->blink_is_real = (term->esc_args[0] >= 1);
+                        term_schedule_tblink(term);
+                        break;
+                      case ANSI('F', '='):      /* set normal foreground */
+                        compatibility(SCOANSI);
+                        if (term->esc_args[0] < 16) {
+                            long colour =
+                                (sco2ansicolour[term->esc_args[0] & 0x7] |
+                                 (term->esc_args[0] & 0x8)) <<
+                                ATTR_FGSHIFT;
+                            term->curr_attr &= ~ATTR_FGMASK;
+                            term->curr_attr |= colour;
+                            term->curr_truecolour.fg = optionalrgb_none;
+                            term->default_attr &= ~ATTR_FGMASK;
+                            term->default_attr |= colour;
+                            set_erase_char(term);
+                        }
+                        break;
+                      case ANSI('G', '='):      /* set normal background */
+                        compatibility(SCOANSI);
+                        if (term->esc_args[0] < 16) {
+                            long colour =
+                                (sco2ansicolour[term->esc_args[0] & 0x7] |
+                                 (term->esc_args[0] & 0x8)) <<
+                                ATTR_BGSHIFT;
+                            term->curr_attr &= ~ATTR_BGMASK;
+                            term->curr_attr |= colour;
+                            term->curr_truecolour.bg = optionalrgb_none;
+                            term->default_attr &= ~ATTR_BGMASK;
+                            term->default_attr |= colour;
+                            set_erase_char(term);
+                        }
+                        break;
+                      case ANSI('L', '='):
+                        compatibility(SCOANSI);
+                        term->use_bce = (term->esc_args[0] <= 0);
+                        set_erase_char(term);
+                        break;
+                      case ANSI('p', '"'): /* DECSCL: set compat level */
+                        /*
+                         * Allow the host to make this emulator a
+                         * 'perfect' VT102. This first appeared in
+                         * the VT220, but we do need to get back to
+                         * PuTTY mode so I won't check it.
+                         *
+                         * The arg in 40..42,50 are a PuTTY extension.
+                         * The 2nd arg, 8bit vs 7bit is not checked.
+                         *
+                         * Setting VT102 mode should also change
+                         * the Fkeys to generate PF* codes as a
+                         * real VT102 has no Fkeys. The VT220 does
+                         * this, F11..F13 become ESC,BS,LF other
+                         * Fkeys send nothing.
+                         *
+                         * Note ESC c will NOT change this!
+                         */
+
+                        switch (term->esc_args[0]) {
+                          case 61:
+                            term->compatibility_level &= ~TM_VTXXX;
+                            term->compatibility_level |= TM_VT102;
+                            break;
+                          case 62:
+                            term->compatibility_level &= ~TM_VTXXX;
+                            term->compatibility_level |= TM_VT220;
+                            break;
+
+                          default:
+                            if (term->esc_args[0] > 60 &&
+                                term->esc_args[0] < 70)
+                                term->compatibility_level |= TM_VTXXX;
+                            break;
+
+                          case 40:
+                            term->compatibility_level &= TM_VTXXX;
+                            break;
+                          case 41:
+                            term->compatibility_level = TM_PUTTY;
+                            break;
+                          case 42:
+                            term->compatibility_level = TM_SCOANSI;
+                            break;
+
+                          case ARG_DEFAULT:
+                            term->compatibility_level = TM_PUTTY;
+                            break;
+                          case 50:
+                            break;
+                        }
+
+                        /* Change the response to CSI c */
+                        if (term->esc_args[0] == 50) {
+                            int i;
+                            char lbuf[64];
+                            strcpy(term->id_string, "\033[?");
+                            for (i = 1; i < term->esc_nargs; i++) {
+                                if (i != 1)
+                                    strcat(term->id_string, ";");
+                                sprintf(lbuf, "%u", term->esc_args[i]);
+                                strcat(term->id_string, lbuf);
+                            }
+                            strcat(term->id_string, "c");
+                        }
+#if 0
+                        /* Is this a good idea ?
+                         * Well we should do a soft reset at this point ...
+                         */
+                        if (!has_compat(VT420) && has_compat(VT100)) {
+                            if (!term->no_remote_resize)
+                                term_request_resize(term,
+                                                    term->reset_132 ? 132 : 80,
+                                                    24);
+                        }
+#endif
+                        break;
+                    }
+                break;
+              case SEEN_OSC:
+                term->osc_w = false;
+                switch (c) {
+                  case 'P':            /* Linux palette sequence */
+                    term->termstate = SEEN_OSC_P;
+                    term->osc_strlen = 0;
+                    break;
+                  case 'R':            /* Linux palette reset */
+                    palette_reset(term, false);
+                    term_invalidate(term);
+                    term->termstate = TOPLEVEL;
+                    break;
+                  case 'W':            /* word-set */
+                    term->termstate = SEEN_OSC_W;
+                    term->osc_w = true;
+                    break;
+                  case '0':
+                  case '1':
+                  case '2':
+                  case '3':
+                  case '4':
+                  case '5':
+                  case '6':
+                  case '7':
+                  case '8':
+                  case '9':
+                    if (term->esc_args[term->esc_nargs-1] <= UINT_MAX / 10 &&
+                        term->esc_args[term->esc_nargs-1] * 10 <= UINT_MAX - c - '0')
+                        term->esc_args[term->esc_nargs-1] =
+                            10 * term->esc_args[term->esc_nargs-1] + c - '0';
+                    else
+                        term->esc_args[term->esc_nargs-1] = UINT_MAX;
+                    break;
+                  case 0x9C:
+                    /* Terminate even though we aren't in OSC_STRING yet */
+                    do_osc(term);
+                    term->termstate = TOPLEVEL;
+                    break;
+                  case 0xC2:
+                    if (in_utf(term)) {
+                        /* Or be prepared for the UTF-8 version of that */
+                        term->termstate = OSC_MAYBE_ST_UTF8;
+                    }
+                    break;
+                  default:
+                    /*
+                     * _Most_ other characters here terminate the
+                     * immediate parsing of the OSC sequence and go
+                     * into OSC_STRING state, but we deal with a
+                     * couple of exceptions first.
+                     */
+                    if (c == 'L' && term->esc_args[0] == 2) {
+                        /*
+                         * Grotty hack to support xterm and DECterm title
+                         * sequences concurrently.
+                         */
+                        term->esc_args[0] = 1;
+                    } else if (c == ';' && term->esc_nargs == 1 &&
+                               term->esc_args[0] == 4) {
+                        /*
+                         * xterm's OSC 4 sequence to query the current
+                         * RGB value of a colour takes a second
+                         * numeric argument which is easiest to parse
+                         * using the existing system rather than in
+                         * do_osc.
+                         */
+                        term->esc_args[term->esc_nargs++] = 0;
+                    } else {
+                        term->termstate = OSC_STRING;
+                        term->osc_strlen = 0;
+                    }
+                }
+                break;
+              case OSC_STRING:
+                /*
+                 * OSC sequences can be terminated or aborted in
+                 * various ways.
+                 *
+                 * The official way to terminate an OSC, per written
+                 * standards, is the String Terminator, SC. That can
+                 * appear in a 7-bit two-character form ESC \, or as
+                 * an 8-bit C1 control 0x9C.
+                 *
+                 * We only accept 0x9C in circumstances where it
+                 * doesn't interfere with our main character set
+                 * processing: so in ISO 8859-1, for example, the byte
+                 * 0x9C is interpreted as ST, but in CP437 it's
+                 * interpreted as an ordinary printing character (as
+                 * it happens, the pound sign), because you might
+                 * perfectly well want to put it in the window title
+                 * like any other printing character.
+                 *
+                 * In particular, in UTF-8 mode, 0x9C is a perfectly
+                 * valid continuation byte for an ordinary printing
+                 * character, so we don't accept the C1 control form
+                 * of ST unless it appears as a full UTF-8 character
+                 * in its own right, i.e. bytes 0xC2 0x9C.
+                 *
+                 * BEL is also treated as a clean termination of OSC,
+                 * which I believe was a behaviour introduced by
+                 * xterm.
+                 *
+                 * To prevent run-on storage of OSC data forever if
+                 * emission of a control sequence is interrupted, we
+                 * also treat various control characters as illegal,
+                 * so that they abort the OSC without processing it
+                 * and return to TOPLEVEL state. These are CR, LF, and
+                 * any ESC that is *not* followed by \.
+                 */
+
+                if (c == '\012' || c == '\015') {
+                    /* CR or LF aborts */
+                    term->termstate = TOPLEVEL;
+                    break;
+                }
+
+                if (c == '\033') {
+                    /* ESC goes into a state where we wait to see if
+                     * the next character is \ */
+                    term->termstate = OSC_MAYBE_ST;
+                    break;
+                }
+
+                if (c == '\007' || (c == 0x9C && !in_utf(term) &&
+                                    term->ucsdata->unitab_ctrl[c] != 0xFF)) {
+                    /* BEL, or the C1 ST appearing as a one-byte
+                     * encoding, cleanly terminates the OSC right here */
+                    do_osc(term);
+                    term->termstate = TOPLEVEL;
+                    break;
+                }
+
+                if (c == 0xC2 && in_utf(term)) {
+                    /* 0xC2 is the UTF-8 character that might
+                     * introduce the encoding of C1 ST */
+                    term->termstate = OSC_MAYBE_ST_UTF8;
+                    break;
+                }
+
+                /* Anything else gets added to the string */
+                if (term->osc_strlen < OSC_STR_MAX)
+                    term->osc_string[term->osc_strlen++] = (char)c;
+                break;
+              case OSC_MAYBE_ST_UTF8:
+                /* In UTF-8 mode, we've seen C2, so are we now seeing
+                 * 9C? */
+                if (c == 0x9C) {
+                    /* Yes, so cleanly terminate the OSC */
+                    do_osc(term);
+                    term->termstate = TOPLEVEL;
+                    break;
+                }
+                /* No, so append the pending C2 byte to the OSC string
+                 * followed by the current character, and go back to
+                 * OSC string accumulation */
+                if (term->osc_strlen < OSC_STR_MAX)
+                    term->osc_string[term->osc_strlen++] = 0xC2;
+                if (term->osc_strlen < OSC_STR_MAX)
+                    term->osc_string[term->osc_strlen++] = (char)c;
+                term->termstate = OSC_STRING;
+                break;
+              case SEEN_OSC_P: {
+                int max = (term->osc_strlen == 0 ? 21 : 15);
+                int val;
+                if ((int)c >= '0' && (int)c <= '9')
+                    val = c - '0';
+                else if ((int)c >= 'A' && (int)c <= 'A' + max - 10)
+                    val = c - 'A' + 10;
+                else if ((int)c >= 'a' && (int)c <= 'a' + max - 10)
+                    val = c - 'a' + 10;
+                else {
+                    term->termstate = TOPLEVEL;
+                    break;
+                }
+                term->osc_string[term->osc_strlen++] = val;
+                if (term->osc_strlen >= 7) {
+                    unsigned oscp_index = term->osc_string[0];
+                    assert(oscp_index < OSCP_NCOLOURS);
+                    unsigned osc4_index =
+                        colour_indices_oscp_to_osc4[oscp_index];
+
+                    rgb *value = &term->subpalettes[SUBPAL_SESSION].values[
+                        osc4_index];
+                    value->r = term->osc_string[1] * 16 + term->osc_string[2];
+                    value->g = term->osc_string[3] * 16 + term->osc_string[4];
+                    value->b = term->osc_string[5] * 16 + term->osc_string[6];
+                    term->subpalettes[SUBPAL_SESSION].present[
+                        osc4_index] = true;
+
+                    palette_rebuild(term);
+
+                    term->termstate = TOPLEVEL;
+                }
+                break;
+              }
+              case SEEN_OSC_W:
+                switch (c) {
+                  case '0':
+                  case '1':
+                  case '2':
+                  case '3':
+                  case '4':
+                  case '5':
+                  case '6':
+                  case '7':
+                  case '8':
+                  case '9':
+                    if (term->esc_args[0] <= UINT_MAX / 10 &&
+                        term->esc_args[0] * 10 <= UINT_MAX - c - '0')
+                        term->esc_args[0] = 10 * term->esc_args[0] + c - '0';
+                    else
+                        term->esc_args[0] = UINT_MAX;
+                    break;
+                  case 0x9C:
+                    /* Terminate even though we aren't in OSC_STRING yet */
+                    do_osc(term);
+                    term->termstate = TOPLEVEL;
+                    break;
+                  case 0xC2:
+                    if (in_utf(term)) {
+                        /* Or be prepared for the UTF-8 version of that */
+                        term->termstate = OSC_MAYBE_ST_UTF8;
+                    }
+                    break;
+                  default:
+                    term->termstate = OSC_STRING;
+                    term->osc_strlen = 0;
+                }
+                break;
+              case VT52_ESC:
+                term->termstate = TOPLEVEL;
+                switch (c) {
+                  case 'A':
+                    move(term, term->curs.x, term->curs.y - 1, 1);
+                    break;
+                  case 'B':
+                    move(term, term->curs.x, term->curs.y + 1, 1);
+                    break;
+                  case 'C':
+                    move(term, term->curs.x + 1, term->curs.y, 1);
+                    break;
+                  case 'D':
+                    move(term, term->curs.x - 1, term->curs.y, 1);
+                    break;
+                    /*
+                     * From the VT100 Manual
+                     * NOTE: The special graphics characters in the VT100
+                     *       are different from those in the VT52
+                     *
+                     * From VT102 manual:
+                     *       137 _  Blank             - Same
+                     *       140 `  Reserved          - Humm.
+                     *       141 a  Solid rectangle   - Similar
+                     *       142 b  1/                - Top half of fraction for the
+                     *       143 c  3/                - subscript numbers below.
+                     *       144 d  5/
+                     *       145 e  7/
+                     *       146 f  Degrees           - Same
+                     *       147 g  Plus or minus     - Same
+                     *       150 h  Right arrow
+                     *       151 i  Ellipsis (dots)
+                     *       152 j  Divide by
+                     *       153 k  Down arrow
+                     *       154 l  Bar at scan 0
+                     *       155 m  Bar at scan 1
+                     *       156 n  Bar at scan 2
+                     *       157 o  Bar at scan 3     - Similar
+                     *       160 p  Bar at scan 4     - Similar
+                     *       161 q  Bar at scan 5     - Similar
+                     *       162 r  Bar at scan 6     - Same
+                     *       163 s  Bar at scan 7     - Similar
+                     *       164 t  Subscript 0
+                     *       165 u  Subscript 1
+                     *       166 v  Subscript 2
+                     *       167 w  Subscript 3
+                     *       170 x  Subscript 4
+                     *       171 y  Subscript 5
+                     *       172 z  Subscript 6
+                     *       173 {  Subscript 7
+                     *       174 |  Subscript 8
+                     *       175 }  Subscript 9
+                     *       176 ~  Paragraph
+                     *
+                     */
+                  case 'F':
+                    term->cset_attr[term->cset = 0] = CSET_LINEDRW;
+                    break;
+                  case 'G':
+                    term->cset_attr[term->cset = 0] = CSET_ASCII;
+                    break;
+                  case 'H':
+                    move(term, 0, 0, 0);
+                    break;
+                  case 'I':
+                    if (term->curs.y == 0) {
+                        scroll(term, 0, term->rows - 1, -1, true);
+                    } else if (term->curs.y > 0) {
+                        term->curs.y--;
+                        seen_disp_event(term);
+                    }
+                    term->wrapnext = false;
+                    break;
+                  case 'J':
+                    erase_lots(term, false, false, true);
+                    if (term->scroll_on_disp)
+                        term->disptop = 0;
+                    break;
+                  case 'K':
+                    erase_lots(term, true, false, true);
+                    break;
+#if 0
+                  case 'V':
+                    /* XXX Print cursor line */
+                    break;
+                  case 'W':
+                    /* XXX Start controller mode */
+                    break;
+                  case 'X':
+                    /* XXX Stop controller mode */
+                    break;
+#endif
+                  case 'Y':
+                    term->termstate = VT52_Y1;
+                    break;
+                  case 'Z':
+                    if (term->ldisc)
+                        ldisc_send(term->ldisc, "\033/Z", 3, false);
+                    break;
+                  case '=':
+                    term->app_keypad_keys = true;
+                    break;
+                  case '>':
+                    term->app_keypad_keys = false;
+                    break;
+                  case '<':
+                    /* XXX This should switch to VT100 mode not current or default
+                     *     VT mode. But this will only have effect in a VT220+
+                     *     emulation.
+                     */
+                    term->vt52_mode = false;
+                    term->blink_is_real = term->blinktext;
+                    term_schedule_tblink(term);
+                    break;
+#if 0
+                  case '^':
+                    /* XXX Enter auto print mode */
+                    break;
+                  case '_':
+                    /* XXX Exit auto print mode */
+                    break;
+                  case ']':
+                    /* XXX Print screen */
+                    break;
+#endif
+
+#ifdef VT52_PLUS
+                  case 'E':
+                    /* compatibility(ATARI) */
+                    move(term, 0, 0, 0);
+                    erase_lots(term, false, false, true);
+                    if (term->scroll_on_disp)
+                        term->disptop = 0;
+                    break;
+                  case 'L':
+                    /* compatibility(ATARI) */
+                    if (term->curs.y <= term->marg_b)
+                        scroll(term, term->curs.y, term->marg_b, -1, false);
+                    break;
+                  case 'M':
+                    /* compatibility(ATARI) */
+                    if (term->curs.y <= term->marg_b)
+                        scroll(term, term->curs.y, term->marg_b, 1, true);
+                    break;
+                  case 'b':
+                    /* compatibility(ATARI) */
+                    term->termstate = VT52_FG;
+                    break;
+                  case 'c':
+                    /* compatibility(ATARI) */
+                    term->termstate = VT52_BG;
+                    break;
+                  case 'd':
+                    /* compatibility(ATARI) */
+                    erase_lots(term, false, true, false);
+                    if (term->scroll_on_disp)
+                        term->disptop = 0;
+                    break;
+                  case 'e':
+                    /* compatibility(ATARI) */
+                    term->cursor_on = true;
+                    seen_disp_event(term);
+                    break;
+                  case 'f':
+                    /* compatibility(ATARI) */
+                    term->cursor_on = false;
+                    seen_disp_event(term);
+                    break;
+                    /* case 'j': Save cursor position - broken on ST */
+                    /* case 'k': Restore cursor position */
+                  case 'l':
+                    /* compatibility(ATARI) */
+                    erase_lots(term, true, true, true);
+                    term->curs.x = 0;
+                    term->wrapnext = false;
+                    break;
+                  case 'o':
+                    /* compatibility(ATARI) */
+                    erase_lots(term, true, true, false);
+                    break;
+                  case 'p':
+                    /* compatibility(ATARI) */
+                    term->curr_attr |= ATTR_REVERSE;
+                    break;
+                  case 'q':
+                    /* compatibility(ATARI) */
+                    term->curr_attr &= ~ATTR_REVERSE;
+                    break;
+                  case 'v':            /* wrap Autowrap on - Wyse style */
+                    /* compatibility(ATARI) */
+                    term->wrap = true;
+                    break;
+                  case 'w':            /* Autowrap off */
+                    /* compatibility(ATARI) */
+                    term->wrap = false;
+                    break;
+
+                  case 'R':
+                    /* compatibility(OTHER) */
+                    term->vt52_bold = false;
+                    term->curr_attr = ATTR_DEFAULT;
+                    term->curr_truecolour.fg = optionalrgb_none;
+                    term->curr_truecolour.bg = optionalrgb_none;
+                    set_erase_char(term);
+                    break;
+                  case 'S':
+                    /* compatibility(VI50) */
+                    term->curr_attr |= ATTR_UNDER;
+                    break;
+                  case 'W':
+                    /* compatibility(VI50) */
+                    term->curr_attr &= ~ATTR_UNDER;
+                    break;
+                  case 'U':
+                    /* compatibility(VI50) */
+                    term->vt52_bold = true;
+                    term->curr_attr |= ATTR_BOLD;
+                    break;
+                  case 'T':
+                    /* compatibility(VI50) */
+                    term->vt52_bold = false;
+                    term->curr_attr &= ~ATTR_BOLD;
+                    break;
+#endif
+                }
+                break;
+              case VT52_Y1:
+                term->termstate = VT52_Y2;
+                move(term, term->curs.x, c - ' ', 0);
+                break;
+              case VT52_Y2:
+                term->termstate = TOPLEVEL;
+                move(term, c - ' ', term->curs.y, 0);
+                break;
+
+#ifdef VT52_PLUS
+              case VT52_FG:
+                term->termstate = TOPLEVEL;
+                term->curr_attr &= ~ATTR_FGMASK;
+                term->curr_attr &= ~ATTR_BOLD;
+                term->curr_attr |= (c & 0xF) << ATTR_FGSHIFT;
+                set_erase_char(term);
+                break;
+              case VT52_BG:
+                term->termstate = TOPLEVEL;
+                term->curr_attr &= ~ATTR_BGMASK;
+                term->curr_attr &= ~ATTR_BLINK;
+                term->curr_attr |= (c & 0xF) << ATTR_BGSHIFT;
+                set_erase_char(term);
+                break;
+#endif
+              default: break;          /* placate gcc warning about enum use */
+            }
+        if (term->selstate != NO_SELECTION) {
+            pos cursplus = term->curs;
+            incpos(cursplus);
+            check_selection(term, term->curs, cursplus);
+        }
+    }
+
+    bufchain_consume(&term->inbuf, nchars_used);
+
+    if (!called_from_term_data)
+        win_unthrottle(term->win, bufchain_size(&term->inbuf));
+
+    term_print_flush(term);
+    if (term->logflush && term->logctx)
+        logflush(term->logctx);
+}
+
+/* Wrapper on term_out with the right prototype to be a toplevel callback */
+void term_out_cb(void *ctx)
+{
+    term_out((Terminal *)ctx, false);
+}
+
+/*
+ * Small subroutine to parse three consecutive escape-sequence
+ * arguments representing a true-colour RGB triple into an
+ * optionalrgb.
+ */
+static void parse_optionalrgb(optionalrgb *out, unsigned *values)
+{
+    out->enabled = true;
+    out->r = values[0] < 256 ? values[0] : 0;
+    out->g = values[1] < 256 ? values[1] : 0;
+    out->b = values[2] < 256 ? values[2] : 0;
+}
+
+/*
+ * To prevent having to run the reasonably tricky bidi algorithm
+ * too many times, we maintain a cache of the last lineful of data
+ * fed to the algorithm on each line of the display.
+ */
+static bool term_bidi_cache_hit(Terminal *term, int line,
+                                termchar *lbefore, int width, bool trusted)
+{
+    int i;
+
+    if (!term->pre_bidi_cache)
+        return false;                  /* cache doesn't even exist yet! */
+
+    if (line >= term->bidi_cache_size)
+        return false;                  /* cache doesn't have this many lines */
+
+    if (!term->pre_bidi_cache[line].chars)
+        return false;                  /* cache doesn't contain _this_ line */
+
+    if (term->pre_bidi_cache[line].width != width)
+        return false;                  /* line is wrong width */
+
+    if (term->pre_bidi_cache[line].trusted != trusted)
+        return false;                  /* line has wrong trust state */
+
+    for (i = 0; i < width; i++)
+        if (!termchars_equal(term->pre_bidi_cache[line].chars+i, lbefore+i))
+            return false;              /* line doesn't match cache */
+
+    return true;                       /* it didn't match. */
+}
+
+static void term_bidi_cache_store(Terminal *term, int line, termchar *lbefore,
+                                  termchar *lafter, bidi_char *wcTo,
+                                  int width, int size, bool trusted)
+{
+    size_t i, j;
+
+    if (!term->pre_bidi_cache || term->bidi_cache_size <= line) {
+        j = term->bidi_cache_size;
+        sgrowarray(term->pre_bidi_cache, term->bidi_cache_size, line);
+        term->post_bidi_cache = sresize(term->post_bidi_cache,
+                                        term->bidi_cache_size,
+                                        struct bidi_cache_entry);
+        while (j < term->bidi_cache_size) {
+            term->pre_bidi_cache[j].chars =
+                term->post_bidi_cache[j].chars = NULL;
+            term->pre_bidi_cache[j].width =
+                term->post_bidi_cache[j].width = -1;
+            term->pre_bidi_cache[j].trusted = false;
+            term->post_bidi_cache[j].trusted = false;
+            term->pre_bidi_cache[j].forward =
+                term->post_bidi_cache[j].forward = NULL;
+            term->pre_bidi_cache[j].backward =
+                term->post_bidi_cache[j].backward = NULL;
+            j++;
+        }
+    }
+
+    sfree(term->pre_bidi_cache[line].chars);
+    sfree(term->post_bidi_cache[line].chars);
+    sfree(term->post_bidi_cache[line].forward);
+    sfree(term->post_bidi_cache[line].backward);
+
+    term->pre_bidi_cache[line].width = width;
+    term->pre_bidi_cache[line].trusted = trusted;
+    term->pre_bidi_cache[line].chars = snewn(size, termchar);
+    term->post_bidi_cache[line].width = width;
+    term->post_bidi_cache[line].trusted = trusted;
+    term->post_bidi_cache[line].chars = snewn(size, termchar);
+    term->post_bidi_cache[line].forward = snewn(width, int);
+    term->post_bidi_cache[line].backward = snewn(width, int);
+
+    memcpy(term->pre_bidi_cache[line].chars, lbefore, size * TSIZE);
+    memcpy(term->post_bidi_cache[line].chars, lafter, size * TSIZE);
+    memset(term->post_bidi_cache[line].forward, 0, width * sizeof(int));
+    memset(term->post_bidi_cache[line].backward, 0, width * sizeof(int));
+
+    for (i = j = 0; j < width; j += wcTo[i].nchars, i++) {
+        int p = wcTo[i].index;
+
+        if (p != BIDI_CHAR_INDEX_NONE) {
+            assert(0 <= p && p < width);
+
+            for (int x = 0; x < wcTo[i].nchars; x++) {
+                term->post_bidi_cache[line].backward[j+x] = p+x;
+                term->post_bidi_cache[line].forward[p+x] = j+x;
+            }
+        }
+    }
+}
+
+/*
+ * Prepare the bidi information for a screen line. Returns the
+ * transformed list of termchars, or NULL if no transformation at
+ * all took place (because bidi is disabled). If return was
+ * non-NULL, auxiliary information such as the forward and reverse
+ * mappings of permutation position are available in
+ * term->post_bidi_cache[scr_y].*.
+ */
+static termchar *term_bidi_line(Terminal *term, struct termline *ldata,
+                                int scr_y)
+{
+    termchar *lchars;
+    int it;
+
+    /* Do Arabic shaping and bidi. */
+    if (!term->no_bidi || !term->no_arabicshaping ||
+        (ldata->trusted && term->cols > TRUST_SIGIL_WIDTH)) {
+
+        if (!term_bidi_cache_hit(term, scr_y, ldata->chars, term->cols,
+                                 ldata->trusted)) {
+
+            if (term->wcFromTo_size < term->cols) {
+                term->wcFromTo_size = term->cols;
+                term->wcFrom = sresize(term->wcFrom, term->wcFromTo_size,
+                                       bidi_char);
+                term->wcTo = sresize(term->wcTo, term->wcFromTo_size,
+                                     bidi_char);
+            }
+
+            for(it=0; it<term->cols ; it++)
+            {
+                unsigned long uc = (ldata->chars[it].chr);
+
+                switch (uc & CSET_MASK) {
+                  case CSET_LINEDRW:
+                    if (!term->rawcnp) {
+                        uc = term->ucsdata->unitab_xterm[uc & 0xFF];
+                        break;
+                    }
+                  case CSET_ASCII:
+                    uc = term->ucsdata->unitab_line[uc & 0xFF];
+                    break;
+                  case CSET_SCOACS:
+                    uc = term->ucsdata->unitab_scoacs[uc&0xFF];
+                    break;
+                }
+                switch (uc & CSET_MASK) {
+                  case CSET_ACP:
+                    uc = term->ucsdata->unitab_font[uc & 0xFF];
+                    break;
+                  case CSET_OEMCP:
+                    uc = term->ucsdata->unitab_oemcp[uc & 0xFF];
+                    break;
+                }
+
+                term->wcFrom[it].origwc = term->wcFrom[it].wc =
+                    (unsigned int)uc;
+                term->wcFrom[it].index = it;
+                term->wcFrom[it].nchars = 1;
+            }
+
+            if (ldata->trusted && term->cols > TRUST_SIGIL_WIDTH) {
+                memmove(
+                    term->wcFrom + TRUST_SIGIL_WIDTH, term->wcFrom,
+                    (term->cols - TRUST_SIGIL_WIDTH) * sizeof(*term->wcFrom));
+                for (it = 0; it < TRUST_SIGIL_WIDTH; it++) {
+                    term->wcFrom[it].origwc = term->wcFrom[it].wc =
+                        (it == 0 ? TRUST_SIGIL_CHAR :
+                         it == 1 ? UCSWIDE : ' ');
+                    term->wcFrom[it].index = BIDI_CHAR_INDEX_NONE;
+                    term->wcFrom[it].nchars = 1;
+                }
+            }
+
+            int nbc = 0;
+            for (it = 0; it < term->cols; it++) {
+                term->wcFrom[nbc] = term->wcFrom[it];
+                if (it+1 < term->cols && term->wcFrom[it+1].wc == UCSWIDE) {
+                    term->wcFrom[nbc].nchars++;
+                    it++;
+                }
+                nbc++;
+            }
+
+            if(!term->no_bidi)
+                do_bidi(term->bidi_ctx, term->wcFrom, nbc);
+
+            if(!term->no_arabicshaping) {
+                do_shape(term->wcFrom, term->wcTo, nbc);
+            } else {
+                /* If we're not calling do_shape, we must copy the
+                 * data into wcTo anyway, unchanged */
+                memcpy(term->wcTo, term->wcFrom, nbc * sizeof(*term->wcTo));
+            }
+
+            if (term->ltemp_size < ldata->size) {
+                term->ltemp_size = ldata->size;
+                term->ltemp = sresize(term->ltemp, term->ltemp_size,
+                                      termchar);
+            }
+
+            memcpy(term->ltemp, ldata->chars, ldata->size * TSIZE);
+
+            int opos = 0;
+            for (it=0; it<nbc; it++) {
+                int ipos = term->wcTo[it].index;
+                for (int j = 0; j < term->wcTo[it].nchars; j++) {
+                    if (ipos != BIDI_CHAR_INDEX_NONE) {
+                        term->ltemp[opos] = ldata->chars[ipos];
+                        if (term->ltemp[opos].cc_next)
+                            term->ltemp[opos].cc_next -= opos - ipos;
+
+                        if (j > 0)
+                            term->ltemp[opos].chr = UCSWIDE;
+                        else if (term->wcTo[it].origwc != term->wcTo[it].wc)
+                            term->ltemp[opos].chr = term->wcTo[it].wc;
+                    } else {
+                        term->ltemp[opos] = term->basic_erase_char;
+                        term->ltemp[opos].chr =
+                            j > 0 ? UCSWIDE : term->wcTo[it].origwc;
+                    }
+                    opos++;
+                }
+            }
+            assert(opos == term->cols);
+            term_bidi_cache_store(term, scr_y, ldata->chars,
+                                  term->ltemp, term->wcTo,
+                                  term->cols, ldata->size, ldata->trusted);
+
+            lchars = term->ltemp;
+        } else {
+            lchars = term->post_bidi_cache[scr_y].chars;
+        }
+    } else {
+        lchars = NULL;
+    }
+
+    return lchars;
+}
+
+static void do_paint_draw(Terminal *term, termline *ldata, int x, int y,
+                          wchar_t *ch, int ccount,
+                          unsigned long attr, truecolour tc)
+{
+    if (ch[0] == TRUST_SIGIL_CHAR) {
+        assert(ldata->trusted);
+        assert(ccount == 1);
+        assert(attr & ATTR_WIDE);
+        wchar_t tch[2];
+        tch[0] = tch[1] = L' ';
+        win_draw_text(term->win, x, y, tch, 2, term->basic_erase_char.attr,
+                      ldata->lattr, term->basic_erase_char.truecolour);
+        win_draw_trust_sigil(term->win, x, y);
+    } else {
+        win_draw_text(term->win, x, y, ch, ccount, attr, ldata->lattr, tc);
+        if (attr & (TATTR_ACTCURS | TATTR_PASCURS))
+            win_draw_cursor(term->win, x, y, ch, ccount,
+                            attr, ldata->lattr, tc);
+    }
+}
+
+/*
+ * Given a context, update the window.
+ */
+static void do_paint(Terminal *term)
+{
+    int i, j, our_curs_y, our_curs_x;
+    int rv, cursor;
+    pos scrpos;
+    wchar_t *ch;
+    size_t chlen;
+    termchar *newline;
+
+    chlen = 1024;
+    ch = snewn(chlen, wchar_t);
+
+    newline = snewn(term->cols, termchar);
+
+    rv = (!term->rvideo ^ !term->in_vbell ? ATTR_REVERSE : 0);
+
+    /* Depends on:
+     * screen array, disptop, scrtop,
+     * selection, rv,
+     * blinkpc, blink_is_real, tblinker,
+     * curs.y, curs.x, cblinker, blink_cur, cursor_on, has_focus, wrapnext
+     */
+
+    /* Has the cursor position or type changed ? */
+    if (term->cursor_on) {
+        if (term->has_focus) {
+            if (term->cblinker || !term->blink_cur)
+                cursor = TATTR_ACTCURS;
+            else
+                cursor = 0;
+        } else
+            cursor = TATTR_PASCURS;
+        if (term->wrapnext)
+            cursor |= TATTR_RIGHTCURS;
+    } else
+        cursor = 0;
+    our_curs_y = term->curs.y - term->disptop;
+    {
+        /*
+         * Adjust the cursor position:
+         *  - for bidi
+         *  - in the case where it's resting on the right-hand half
+         *    of a CJK wide character. xterm's behaviour here,
+         *    which seems adequate to me, is to display the cursor
+         *    covering the _whole_ character, exactly as if it were
+         *    one space to the left.
+         */
+        termline *ldata = lineptr(term->curs.y);
+        termchar *lchars;
+
+        our_curs_x = term->curs.x;
+
+        if ( (lchars = term_bidi_line(term, ldata, our_curs_y)) != NULL) {
+            our_curs_x = term->post_bidi_cache[our_curs_y].forward[our_curs_x];
+        } else
+            lchars = ldata->chars;
+
+        if (our_curs_x > 0 &&
+            lchars[our_curs_x].chr == UCSWIDE)
+            our_curs_x--;
+
+        unlineptr(ldata);
+    }
+
+    /*
+     * If the cursor is not where it was last time we painted, and
+     * its previous position is visible on screen, invalidate its
+     * previous position.
+     */
+    if (term->dispcursy >= 0 &&
+        (term->curstype != cursor ||
+         term->dispcursy != our_curs_y ||
+         term->dispcursx != our_curs_x)) {
+        termchar *dispcurs = term->disptext[term->dispcursy]->chars +
+            term->dispcursx;
+
+        if (term->dispcursx > 0 && dispcurs->chr == UCSWIDE)
+            dispcurs[-1].attr |= ATTR_INVALID;
+        if (term->dispcursx < term->cols-1 && dispcurs[1].chr == UCSWIDE)
+            dispcurs[1].attr |= ATTR_INVALID;
+        dispcurs->attr |= ATTR_INVALID;
+
+        term->curstype = 0;
+    }
+    term->dispcursx = term->dispcursy = -1;
+
+    /* The normal screen data */
+    for (i = 0; i < term->rows; i++) {
+        termline *ldata;
+        termchar *lchars;
+        bool dirty_line, dirty_run, selected;
+        unsigned long attr = 0, cset = 0;
+        int start = 0;
+        int ccount = 0;
+        bool last_run_dirty = false;
+        int laststart;
+        bool dirtyrect;
+        int *backward;
+        truecolour tc;
+
+        scrpos.y = i + term->disptop;
+        ldata = lineptr(scrpos.y);
+
+        /* Do Arabic shaping and bidi. */
+        lchars = term_bidi_line(term, ldata, i);
+        if (lchars) {
+            backward = term->post_bidi_cache[i].backward;
+        } else {
+            lchars = ldata->chars;
+            backward = NULL;
+        }
+
+        /*
+         * First loop: work along the line deciding what we want
+         * each character cell to look like.
+         */
+        for (j = 0; j < term->cols; j++) {
+            unsigned long tattr, tchar;
+            termchar *d = lchars + j;
+            scrpos.x = backward ? backward[j] : j;
+
+            tchar = d->chr;
+            tattr = d->attr;
+
+            if (!term->ansi_colour)
+                tattr = (tattr & ~(ATTR_FGMASK | ATTR_BGMASK)) |
+                    ATTR_DEFFG | ATTR_DEFBG;
+
+            if (!term->xterm_256_colour) {
+                int colour;
+                colour = (tattr & ATTR_FGMASK) >> ATTR_FGSHIFT;
+                if (colour >= 16 && colour < 256)
+                    tattr = (tattr &~ ATTR_FGMASK) | ATTR_DEFFG;
+                colour = (tattr & ATTR_BGMASK) >> ATTR_BGSHIFT;
+                if (colour >= 16 && colour < 256)
+                    tattr = (tattr &~ ATTR_BGMASK) | ATTR_DEFBG;
+            }
+
+            if (term->true_colour) {
+                tc = d->truecolour;
+            } else {
+                tc.fg = tc.bg = optionalrgb_none;
+            }
+
+            switch (tchar & CSET_MASK) {
+              case CSET_ASCII:
+                tchar = term->ucsdata->unitab_line[tchar & 0xFF];
+                break;
+              case CSET_LINEDRW:
+                tchar = term->ucsdata->unitab_xterm[tchar & 0xFF];
+                break;
+              case CSET_SCOACS:
+                tchar = term->ucsdata->unitab_scoacs[tchar&0xFF];
+                break;
+            }
+            if (j < term->cols-1 && d[1].chr == UCSWIDE)
+                tattr |= ATTR_WIDE;
+
+            /* Video reversing things */
+            if (term->selstate == DRAGGING || term->selstate == SELECTED) {
+                if (term->seltype == LEXICOGRAPHIC)
+                    selected = (posle(term->selstart, scrpos) &&
+                                poslt(scrpos, term->selend));
+                else
+                    selected = (posPle(term->selstart, scrpos) &&
+                                posPle_left(scrpos, term->selend));
+            } else
+                selected = false;
+            tattr = (tattr ^ rv
+                     ^ (selected ? ATTR_REVERSE : 0));
+
+            /* 'Real' blinking ? */
+            if (term->blink_is_real && (tattr & ATTR_BLINK)) {
+                if (term->has_focus && term->tblinker) {
+                    tchar = term->ucsdata->unitab_line[(unsigned char)' '];
+                }
+                tattr &= ~ATTR_BLINK;
+            }
+
+            /*
+             * Check the font we'll _probably_ be using to see if
+             * the character is wide when we don't want it to be.
+             */
+            if (tchar != term->disptext[i]->chars[j].chr ||
+                tattr != (term->disptext[i]->chars[j].attr &~
+                          (ATTR_NARROW | DATTR_MASK))) {
+                if ((tattr & ATTR_WIDE) == 0 &&
+                    win_char_width(term->win, tchar) == 2)
+                    tattr |= ATTR_NARROW;
+            } else if (term->disptext[i]->chars[j].attr & ATTR_NARROW)
+                tattr |= ATTR_NARROW;
+
+            if (i == our_curs_y && j == our_curs_x) {
+                tattr |= cursor;
+                term->curstype = cursor;
+                term->dispcursx = j;
+                term->dispcursy = i;
+            }
+
+            /* FULL-TERMCHAR */
+            newline[j].attr = tattr;
+            newline[j].chr = tchar;
+            newline[j].truecolour = tc;
+            /* Combining characters are still read from lchars */
+            newline[j].cc_next = 0;
+        }
+
+        /*
+         * Now loop over the line again, noting where things have
+         * changed.
+         *
+         * During this loop, we keep track of where we last saw
+         * DATTR_STARTRUN. Any mismatch automatically invalidates
+         * _all_ of the containing run that was last printed: that
+         * is, any rectangle that was drawn in one go in the
+         * previous update should be either left completely alone
+         * or overwritten in its entirety. This, along with the
+         * expectation that front ends clip all text runs to their
+         * bounding rectangle, should solve any possible problems
+         * with fonts that overflow their character cells.
+         */
+        laststart = 0;
+        dirtyrect = false;
+        for (j = 0; j < term->cols; j++) {
+            if (term->disptext[i]->chars[j].attr & DATTR_STARTRUN) {
+                laststart = j;
+                dirtyrect = false;
+            }
+
+            if (term->disptext[i]->chars[j].chr != newline[j].chr ||
+                (term->disptext[i]->chars[j].attr &~ DATTR_MASK)
+                != newline[j].attr) {
+                int k;
+
+                if (!dirtyrect) {
+                    for (k = laststart; k < j; k++)
+                        term->disptext[i]->chars[k].attr |= ATTR_INVALID;
+
+                    dirtyrect = true;
+                }
+            }
+
+            if (dirtyrect)
+                term->disptext[i]->chars[j].attr |= ATTR_INVALID;
+        }
+
+        /*
+         * Finally, loop once more and actually do the drawing.
+         */
+        dirty_run = dirty_line = (ldata->lattr !=
+                                  term->disptext[i]->lattr);
+        term->disptext[i]->lattr = ldata->lattr;
+
+        tc = term->erase_char.truecolour;
+        for (j = 0; j < term->cols; j++) {
+            unsigned long tattr, tchar;
+            bool break_run, do_copy;
+            termchar *d = lchars + j;
+
+            tattr = newline[j].attr;
+            tchar = newline[j].chr;
+
+            if ((term->disptext[i]->chars[j].attr ^ tattr) & ATTR_WIDE)
+                dirty_line = true;
+
+            break_run = ((tattr ^ attr) & term->attr_mask) != 0;
+
+            if (!truecolour_equal(newline[j].truecolour, tc))
+                break_run = true;
+
+#ifdef USES_VTLINE_HACK
+            /* Special hack for VT100 Linedraw glyphs */
+            if ((tchar >= 0x23BA && tchar <= 0x23BD) ||
+                (j > 0 && (newline[j-1].chr >= 0x23BA &&
+                           newline[j-1].chr <= 0x23BD)))
+                break_run = true;
+#endif
+
+            /*
+             * Separate out sequences of characters that have the
+             * same CSET, if that CSET is a magic one.
+             */
+            if (CSET_OF(tchar) != cset)
+                break_run = true;
+
+            /*
+             * Break on both sides of any combined-character cell.
+             */
+            if (d->cc_next != 0 ||
+                (j > 0 && d[-1].cc_next != 0))
+                break_run = true;
+
+            /*
+             * Break on both sides of a trust sigil.
+             */
+            if (d->chr == TRUST_SIGIL_CHAR ||
+                (j >= 2 && d[-1].chr == UCSWIDE &&
+                 d[-2].chr == TRUST_SIGIL_CHAR))
+                break_run = true;
+
+            if (!term->ucsdata->dbcs_screenfont && !dirty_line) {
+                if (term->disptext[i]->chars[j].chr == tchar &&
+                    (term->disptext[i]->chars[j].attr &~ DATTR_MASK)==tattr &&
+                    truecolour_equal(
+                        term->disptext[i]->chars[j].truecolour, tc))
+                    break_run = true;
+                else if (!dirty_run && ccount == 1)
+                    break_run = true;
+            }
+
+            if (break_run) {
+                if ((dirty_run || last_run_dirty) && ccount > 0)
+                    do_paint_draw(term, ldata, start, i, ch, ccount, attr, tc);
+                start = j;
+                ccount = 0;
+                attr = tattr;
+                tc = newline[j].truecolour;
+                cset = CSET_OF(tchar);
+                if (term->ucsdata->dbcs_screenfont)
+                    last_run_dirty = dirty_run;
+                dirty_run = dirty_line;
+            }
+
+            do_copy = false;
+            if (!termchars_equal_override(&term->disptext[i]->chars[j],
+                                          d, tchar, tattr)) {
+                do_copy = true;
+                dirty_run = true;
+            }
+
+            sgrowarrayn(ch, chlen, ccount, 2);
+
+#ifdef PLATFORM_IS_UTF16
+            if (tchar > 0x10000 && tchar < 0x110000) {
+                ch[ccount++] = (wchar_t) HIGH_SURROGATE_OF(tchar);
+                ch[ccount++] = (wchar_t) LOW_SURROGATE_OF(tchar);
+            } else
+#endif /* PLATFORM_IS_UTF16 */
+            ch[ccount++] = (wchar_t) tchar;
+
+            if (d->cc_next) {
+                termchar *dd = d;
+
+                while (dd->cc_next) {
+                    unsigned long schar;
+
+                    dd += dd->cc_next;
+
+                    schar = dd->chr;
+                    switch (schar & CSET_MASK) {
+                      case CSET_ASCII:
+                        schar = term->ucsdata->unitab_line[schar & 0xFF];
+                        break;
+                      case CSET_LINEDRW:
+                        schar = term->ucsdata->unitab_xterm[schar & 0xFF];
+                        break;
+                      case CSET_SCOACS:
+                        schar = term->ucsdata->unitab_scoacs[schar&0xFF];
+                        break;
+                    }
+
+                    sgrowarrayn(ch, chlen, ccount, 2);
+
+#ifdef PLATFORM_IS_UTF16
+                    if (schar > 0x10000 && schar < 0x110000) {
+                        ch[ccount++] = (wchar_t) HIGH_SURROGATE_OF(schar);
+                        ch[ccount++] = (wchar_t) LOW_SURROGATE_OF(schar);
+                    } else
+#endif /* PLATFORM_IS_UTF16 */
+                    ch[ccount++] = (wchar_t) schar;
+                }
+
+                attr |= TATTR_COMBINING;
+            }
+
+            if (do_copy) {
+                copy_termchar(term->disptext[i], j, d);
+                term->disptext[i]->chars[j].chr = tchar;
+                term->disptext[i]->chars[j].attr = tattr;
+                term->disptext[i]->chars[j].truecolour = tc;
+                if (start == j)
+                    term->disptext[i]->chars[j].attr |= DATTR_STARTRUN;
+            }
+
+            /* If it's a wide char step along to the next one. */
+            if (tattr & ATTR_WIDE) {
+                if (++j < term->cols) {
+                    d++;
+                    /*
+                     * By construction above, the cursor should not
+                     * be on the right-hand half of this character.
+                     * Ever.
+                     */
+                    assert(!(i == our_curs_y && j == our_curs_x));
+                    if (!termchars_equal(&term->disptext[i]->chars[j], d))
+                        dirty_run = true;
+                    copy_termchar(term->disptext[i], j, d);
+                }
+            }
+        }
+        if (dirty_run && ccount > 0)
+            do_paint_draw(term, ldata, start, i, ch, ccount, attr, tc);
+
+        unlineptr(ldata);
+    }
+
+    sfree(newline);
+    sfree(ch);
+}
+
+/*
+ * Invalidate the whole screen so it will be repainted in full.
+ */
+void term_invalidate(Terminal *term)
+{
+    int i, j;
+
+    for (i = 0; i < term->rows; i++)
+        for (j = 0; j < term->cols; j++)
+            term->disptext[i]->chars[j].attr |= ATTR_INVALID;
+
+    term_schedule_update(term);
+}
+
+/*
+ * Paint the window in response to a WM_PAINT message.
+ */
+void term_paint(Terminal *term,
+                int left, int top, int right, int bottom, bool immediately)
+{
+    int i, j;
+    if (left < 0) left = 0;
+    if (top < 0) top = 0;
+    if (right >= term->cols) right = term->cols-1;
+    if (bottom >= term->rows) bottom = term->rows-1;
+
+    for (i = top; i <= bottom && i < term->rows; i++) {
+        if ((term->disptext[i]->lattr & LATTR_MODE) == LATTR_NORM)
+            for (j = left; j <= right && j < term->cols; j++)
+                term->disptext[i]->chars[j].attr |= ATTR_INVALID;
+        else
+            for (j = left / 2; j <= right / 2 + 1 && j < term->cols; j++)
+                term->disptext[i]->chars[j].attr |= ATTR_INVALID;
+    }
+
+    if (immediately) {
+        do_paint(term);
+    } else {
+        term_schedule_update(term);
+    }
+}
+
+/*
+ * Attempt to scroll the scrollback. The second parameter gives the
+ * position we want to scroll to; the first is +1 to denote that
+ * this position is relative to the beginning of the scrollback, -1
+ * to denote it is relative to the end, and 0 to denote that it is
+ * relative to the current position.
+ */
+void term_scroll(Terminal *term, int rel, int where)
+{
+    int sbtop = -sblines(term);
+
+    term->disptop = (rel < 0 ? 0 : rel > 0 ? sbtop : term->disptop) + where;
+    if (term->disptop < sbtop)
+        term->disptop = sbtop;
+    if (term->disptop > 0)
+        term->disptop = 0;
+    term->win_scrollbar_update_pending = true;
+    term_schedule_update(term);
+}
+
+/*
+ * Scroll the scrollback to centre it on the beginning or end of the
+ * current selection, if any.
+ */
+void term_scroll_to_selection(Terminal *term, int which_end)
+{
+    pos target;
+    int y;
+    int sbtop = -sblines(term);
+
+    if (term->selstate != SELECTED)
+        return;
+    if (which_end)
+        target = term->selend;
+    else
+        target = term->selstart;
+
+    y = target.y - term->rows/2;
+    if (y < sbtop)
+        y = sbtop;
+    else if (y > 0)
+        y = 0;
+    term_scroll(term, -1, y);
+}
+
+/*
+ * Helper routine for clipme(): growing buffer.
+ */
+typedef struct {
+    size_t bufsize;         /* amount of allocated space in textbuf/attrbuf */
+    size_t bufpos;          /* amount of actual data */
+    wchar_t *textbuf;       /* buffer for copied text */
+    wchar_t *textptr;       /* = textbuf + bufpos (current insertion point) */
+    int *attrbuf;           /* buffer for copied attributes */
+    int *attrptr;           /* = attrbuf + bufpos */
+    truecolour *tcbuf;      /* buffer for copied colours */
+    truecolour *tcptr;      /* = tcbuf + bufpos */
+} clip_workbuf;
+
+static void clip_addchar(clip_workbuf *b, wchar_t chr, int attr, truecolour tc)
+{
+    if (b->bufpos >= b->bufsize) {
+        sgrowarray(b->textbuf, b->bufsize, b->bufpos);
+        b->textptr = b->textbuf + b->bufpos;
+        b->attrbuf = sresize(b->attrbuf, b->bufsize, int);
+        b->attrptr = b->attrbuf + b->bufpos;
+        b->tcbuf = sresize(b->tcbuf, b->bufsize, truecolour);
+        b->tcptr = b->tcbuf + b->bufpos;
+    }
+    *b->textptr++ = chr;
+    *b->attrptr++ = attr;
+    *b->tcptr++ = tc;
+    b->bufpos++;
+}
+
+static void clipme(Terminal *term, pos top, pos bottom, bool rect, bool desel,
+                   const int *clipboards, int n_clipboards)
+{
+    clip_workbuf buf;
+    int old_top_x;
+    int attr;
+    truecolour tc;
+
+    buf.bufsize = 5120;
+    buf.bufpos = 0;
+    buf.textptr = buf.textbuf = snewn(buf.bufsize, wchar_t);
+    buf.attrptr = buf.attrbuf = snewn(buf.bufsize, int);
+    buf.tcptr = buf.tcbuf = snewn(buf.bufsize, truecolour);
+
+    old_top_x = top.x;                 /* needed for rect==1 */
+
+    while (poslt(top, bottom)) {
+        bool nl = false;
+        termline *ldata = lineptr(top.y);
+        pos nlpos;
+
+        /*
+         * nlpos will point at the maximum position on this line we
+         * should copy up to. So we start it at the end of the
+         * line...
+         */
+        nlpos.y = top.y;
+        nlpos.x = term->cols;
+
+        /*
+         * ... move it backwards if there's unused space at the end
+         * of the line (and also set `nl' if this is the case,
+         * because in normal selection mode this means we need a
+         * newline at the end)...
+         */
+        if (!(ldata->lattr & LATTR_WRAPPED)) {
+            while (nlpos.x &&
+                   IS_SPACE_CHR(ldata->chars[nlpos.x - 1].chr) &&
+                   !ldata->chars[nlpos.x - 1].cc_next &&
+                   poslt(top, nlpos))
+                decpos(nlpos);
+            if (poslt(nlpos, bottom))
+                nl = true;
+        } else {
+            if (ldata->trusted) {
+                /* A wrapped line with a trust sigil on it terminates
+                 * a few characters earlier. */
+                nlpos.x = (nlpos.x < TRUST_SIGIL_WIDTH ? 0 :
+                           nlpos.x - TRUST_SIGIL_WIDTH);
+            }
+            if (ldata->lattr & LATTR_WRAPPED2) {
+                /* Ignore the last char on the line in a WRAPPED2 line. */
+                decpos(nlpos);
+            }
+        }
+
+        /*
+         * ... and then clip it to the terminal x coordinate if
+         * we're doing rectangular selection. (In this case we
+         * still did the above, so that copying e.g. the right-hand
+         * column from a table doesn't fill with spaces on the
+         * right.)
+         */
+        if (rect) {
+            if (nlpos.x > bottom.x)
+                nlpos.x = bottom.x;
+            nl = (top.y < bottom.y);
+        }
+
+        while (poslt(top, bottom) && poslt(top, nlpos)) {
+#if 0
+            char cbuf[16], *p;
+            sprintf(cbuf, "<U+%04x>", (ldata[top.x] & 0xFFFF));
+#else
+            wchar_t cbuf[16], *p;
+            int c;
+            int x = top.x;
+
+            if (ldata->chars[x].chr == UCSWIDE) {
+                top.x++;
+                continue;
+            }
+
+            while (1) {
+                int uc = ldata->chars[x].chr;
+                attr = ldata->chars[x].attr;
+                tc = ldata->chars[x].truecolour;
+
+                switch (uc & CSET_MASK) {
+                  case CSET_LINEDRW:
+                    if (!term->rawcnp) {
+                        uc = term->ucsdata->unitab_xterm[uc & 0xFF];
+                        break;
+                    }
+                  case CSET_ASCII:
+                    uc = term->ucsdata->unitab_line[uc & 0xFF];
+                    break;
+                  case CSET_SCOACS:
+                    uc = term->ucsdata->unitab_scoacs[uc&0xFF];
+                    break;
+                }
+                switch (uc & CSET_MASK) {
+                  case CSET_ACP:
+                    uc = term->ucsdata->unitab_font[uc & 0xFF];
+                    break;
+                  case CSET_OEMCP:
+                    uc = term->ucsdata->unitab_oemcp[uc & 0xFF];
+                    break;
+                }
+
+                c = (uc & ~CSET_MASK);
+#ifdef PLATFORM_IS_UTF16
+                if (uc > 0x10000 && uc < 0x110000) {
+                    cbuf[0] = 0xD800 | ((uc - 0x10000) >> 10);
+                    cbuf[1] = 0xDC00 | ((uc - 0x10000) & 0x3FF);
+                    cbuf[2] = 0;
+                } else
+#endif
+                {
+                    cbuf[0] = uc;
+                    cbuf[1] = 0;
+                }
+
+                if (DIRECT_FONT(uc)) {
+                    if (c >= ' ' && c != 0x7F) {
+                        char buf[4];
+                        WCHAR wbuf[4];
+                        int rv;
+                        if (is_dbcs_leadbyte(term->ucsdata->font_codepage, (BYTE) c)) {
+                            buf[0] = c;
+                            buf[1] = (char) (0xFF & ldata->chars[top.x + 1].chr);
+                            rv = mb_to_wc(term->ucsdata->font_codepage, 0, buf, 2, wbuf, 4);
+                            top.x++;
+                        } else {
+                            buf[0] = c;
+                            rv = mb_to_wc(term->ucsdata->font_codepage, 0, buf, 1, wbuf, 4);
+                        }
+
+                        if (rv > 0) {
+                            memcpy(cbuf, wbuf, rv * sizeof(wchar_t));
+                            cbuf[rv] = 0;
+                        }
+                    }
+                }
+#endif
+
+                for (p = cbuf; *p; p++)
+                    clip_addchar(&buf, *p, attr, tc);
+
+                if (ldata->chars[x].cc_next)
+                    x += ldata->chars[x].cc_next;
+                else
+                    break;
+            }
+            top.x++;
+        }
+        if (nl) {
+            int i;
+            for (i = 0; i < sel_nl_sz; i++)
+                clip_addchar(&buf, sel_nl[i], 0, term->basic_erase_char.truecolour);
+        }
+        top.y++;
+        top.x = rect ? old_top_x : 0;
+
+        unlineptr(ldata);
+    }
+#if SELECTION_NUL_TERMINATED
+    clip_addchar(&buf, 0, 0, term->basic_erase_char.truecolour);
+#endif
+    /* Finally, transfer all that to the clipboard(s). */
+    {
+        int i;
+        bool clip_local = false;
+        for (i = 0; i < n_clipboards; i++) {
+            if (clipboards[i] == CLIP_LOCAL) {
+                clip_local = true;
+            } else if (clipboards[i] != CLIP_NULL) {
+                win_clip_write(
+                    term->win, clipboards[i], buf.textbuf, buf.attrbuf,
+                    buf.tcbuf, buf.bufpos, desel);
+            }
+        }
+        if (clip_local) {
+            sfree(term->last_selected_text);
+            sfree(term->last_selected_attr);
+            sfree(term->last_selected_tc);
+            term->last_selected_text = buf.textbuf;
+            term->last_selected_attr = buf.attrbuf;
+            term->last_selected_tc = buf.tcbuf;
+            term->last_selected_len = buf.bufpos;
+        } else {
+            sfree(buf.textbuf);
+            sfree(buf.attrbuf);
+            sfree(buf.tcbuf);
+        }
+    }
+}
+
+void term_copyall(Terminal *term, const int *clipboards, int n_clipboards)
+{
+    pos top;
+    pos bottom;
+    tree234 *screen = term->screen;
+    top.y = -sblines(term);
+    top.x = 0;
+    bottom.y = find_last_nonempty_line(term, screen);
+    bottom.x = term->cols;
+    clipme(term, top, bottom, false, true, clipboards, n_clipboards);
+}
+
+static void paste_from_clip_local(void *vterm)
+{
+    Terminal *term = (Terminal *)vterm;
+    term_do_paste(term, term->last_selected_text, term->last_selected_len);
+}
+
+void term_request_copy(Terminal *term, const int *clipboards, int n_clipboards)
+{
+    int i;
+    for (i = 0; i < n_clipboards; i++) {
+        assert(clipboards[i] != CLIP_LOCAL);
+        if (clipboards[i] != CLIP_NULL) {
+            win_clip_write(term->win, clipboards[i],
+                           term->last_selected_text, term->last_selected_attr,
+                           term->last_selected_tc, term->last_selected_len,
+                           false);
+        }
+    }
+}
+
+void term_request_paste(Terminal *term, int clipboard)
+{
+    switch (clipboard) {
+      case CLIP_NULL:
+        /* Do nothing: CLIP_NULL never has data in it. */
+        break;
+      case CLIP_LOCAL:
+        queue_toplevel_callback(paste_from_clip_local, term);
+        break;
+      default:
+        win_clip_request_paste(term->win, clipboard);
+        break;
+    }
+}
+
+/*
+ * The wordness array is mainly for deciding the disposition of the
+ * US-ASCII characters.
+ */
+static int wordtype(Terminal *term, int uc)
+{
+    struct ucsword {
+        int start, end, ctype;
+    };
+    static const struct ucsword ucs_words[] = {
+        {
+        128, 160, 0}, {
+        161, 191, 1}, {
+        215, 215, 1}, {
+        247, 247, 1}, {
+        0x037e, 0x037e, 1},            /* Greek question mark */
+        {
+        0x0387, 0x0387, 1},            /* Greek ano teleia */
+        {
+        0x055a, 0x055f, 1},            /* Armenian punctuation */
+        {
+        0x0589, 0x0589, 1},            /* Armenian full stop */
+        {
+        0x0700, 0x070d, 1},            /* Syriac punctuation */
+        {
+        0x104a, 0x104f, 1},            /* Myanmar punctuation */
+        {
+        0x10fb, 0x10fb, 1},            /* Georgian punctuation */
+        {
+        0x1361, 0x1368, 1},            /* Ethiopic punctuation */
+        {
+        0x166d, 0x166e, 1},            /* Canadian Syl. punctuation */
+        {
+        0x17d4, 0x17dc, 1},            /* Khmer punctuation */
+        {
+        0x1800, 0x180a, 1},            /* Mongolian punctuation */
+        {
+        0x2000, 0x200a, 0},            /* Various spaces */
+        {
+        0x2070, 0x207f, 2},            /* superscript */
+        {
+        0x2080, 0x208f, 2},            /* subscript */
+        {
+        0x200b, 0x27ff, 1},            /* punctuation and symbols */
+        {
+        0x3000, 0x3000, 0},            /* ideographic space */
+        {
+        0x3001, 0x3020, 1},            /* ideographic punctuation */
+        {
+        0x303f, 0x309f, 3},            /* Hiragana */
+        {
+        0x30a0, 0x30ff, 3},            /* Katakana */
+        {
+        0x3300, 0x9fff, 3},            /* CJK Ideographs */
+        {
+        0xac00, 0xd7a3, 3},            /* Hangul Syllables */
+        {
+        0xf900, 0xfaff, 3},            /* CJK Ideographs */
+        {
+        0xfe30, 0xfe6b, 1},            /* punctuation forms */
+        {
+        0xff00, 0xff0f, 1},            /* half/fullwidth ASCII */
+        {
+        0xff1a, 0xff20, 1},            /* half/fullwidth ASCII */
+        {
+        0xff3b, 0xff40, 1},            /* half/fullwidth ASCII */
+        {
+        0xff5b, 0xff64, 1},            /* half/fullwidth ASCII */
+        {
+        0xfff0, 0xffff, 0},            /* half/fullwidth ASCII */
+        {
+        0, 0, 0}
+    };
+    const struct ucsword *wptr;
+
+    switch (uc & CSET_MASK) {
+      case CSET_LINEDRW:
+        uc = term->ucsdata->unitab_xterm[uc & 0xFF];
+        break;
+      case CSET_ASCII:
+        uc = term->ucsdata->unitab_line[uc & 0xFF];
+        break;
+      case CSET_SCOACS:
+        uc = term->ucsdata->unitab_scoacs[uc&0xFF];
+        break;
+    }
+    switch (uc & CSET_MASK) {
+      case CSET_ACP:
+        uc = term->ucsdata->unitab_font[uc & 0xFF];
+        break;
+      case CSET_OEMCP:
+        uc = term->ucsdata->unitab_oemcp[uc & 0xFF];
+        break;
+    }
+
+    /* For DBCS fonts I can't do anything useful. Even this will sometimes
+     * fail as there's such a thing as a double width space. :-(
+     */
+    if (term->ucsdata->dbcs_screenfont &&
+        term->ucsdata->font_codepage == term->ucsdata->line_codepage)
+        return (uc != ' ');
+
+    if (uc < 0x80)
+        return term->wordness[uc];
+
+    for (wptr = ucs_words; wptr->start; wptr++) {
+        if (uc >= wptr->start && uc <= wptr->end)
+            return wptr->ctype;
+    }
+
+    return 2;
+}
+
+static int line_cols(Terminal *term, termline *ldata)
+{
+    int cols = term->cols;
+    if (ldata->trusted) {
+        cols -= TRUST_SIGIL_WIDTH;
+    }
+    if (ldata->lattr & LATTR_WRAPPED2)
+        cols--;
+    if (cols < 0)
+        cols = 0;
+    return cols;
+}
+
+/*
+ * Spread the selection outwards according to the selection mode.
+ */
+static pos sel_spread_half(Terminal *term, pos p, int dir)
+{
+    termline *ldata;
+    short wvalue;
+    int topy = -sblines(term);
+
+    ldata = lineptr(p.y);
+
+    switch (term->selmode) {
+      case SM_CHAR:
+        /*
+         * In this mode, every character is a separate unit, except
+         * for runs of spaces at the end of a non-wrapping line.
+         */
+        if (!(ldata->lattr & LATTR_WRAPPED)) {
+            termchar *q = ldata->chars + line_cols(term, ldata);
+            while (q > ldata->chars &&
+                   IS_SPACE_CHR(q[-1].chr) && !q[-1].cc_next)
+                q--;
+            if (q == ldata->chars + term->cols)
+                q--;
+            if (p.x >= q - ldata->chars)
+                p.x = (dir == -1 ? q - ldata->chars : term->cols - 1);
+        }
+        break;
+      case SM_WORD:
+        /*
+         * In this mode, the units are maximal runs of characters
+         * whose `wordness' has the same value.
+         */
+        wvalue = wordtype(term, UCSGET(ldata->chars, p.x));
+        if (dir == +1) {
+            while (1) {
+                int maxcols = line_cols(term, ldata);
+                if (p.x < maxcols-1) {
+                    if (wordtype(term, UCSGET(ldata->chars, p.x+1)) == wvalue)
+                        p.x++;
+                    else
+                        break;
+                } else {
+                    if (p.y+1 < term->rows &&
+                        (ldata->lattr & LATTR_WRAPPED)) {
+                        termline *ldata2;
+                        ldata2 = lineptr(p.y+1);
+                        if (wordtype(term, UCSGET(ldata2->chars, 0))
+                            == wvalue) {
+                            p.x = 0;
+                            p.y++;
+                            unlineptr(ldata);
+                            ldata = ldata2;
+                        } else {
+                            unlineptr(ldata2);
+                            break;
+                        }
+                    } else
+                        break;
+                }
+            }
+        } else {
+            while (1) {
+                if (p.x > 0) {
+                    if (wordtype(term, UCSGET(ldata->chars, p.x-1)) == wvalue)
+                        p.x--;
+                    else
+                        break;
+                } else {
+                    termline *ldata2;
+                    int maxcols;
+                    if (p.y <= topy)
+                        break;
+                    ldata2 = lineptr(p.y-1);
+                    maxcols = line_cols(term, ldata2);
+                    if (ldata2->lattr & LATTR_WRAPPED) {
+                        if (wordtype(term, UCSGET(ldata2->chars, maxcols-1))
+                            == wvalue) {
+                            p.x = maxcols-1;
+                            p.y--;
+                            unlineptr(ldata);
+                            ldata = ldata2;
+                        } else {
+                            unlineptr(ldata2);
+                            break;
+                        }
+                    } else
+                        break;
+                }
+            }
+        }
+        break;
+      case SM_LINE:
+        /*
+         * In this mode, every line is a unit.
+         */
+        p.x = (dir == -1 ? 0 : term->cols - 1);
+        break;
+    }
+
+    unlineptr(ldata);
+    return p;
+}
+
+static void sel_spread(Terminal *term)
+{
+    if (term->seltype == LEXICOGRAPHIC) {
+        term->selstart = sel_spread_half(term, term->selstart, -1);
+        decpos(term->selend);
+        term->selend = sel_spread_half(term, term->selend, +1);
+        incpos(term->selend);
+    }
+}
+
+static void term_paste_callback(void *vterm)
+{
+    Terminal *term = (Terminal *)vterm;
+
+    if (term->paste_len == 0)
+        return;
+
+    while (term->paste_pos < term->paste_len) {
+        int n = 0;
+        while (n + term->paste_pos < term->paste_len) {
+            if (term->paste_buffer[term->paste_pos + n++] == '\015')
+                break;
+        }
+        if (term->ldisc) {
+            strbuf *buf = term_input_data_from_unicode(
+                term, term->paste_buffer + term->paste_pos, n);
+            term_keyinput_internal(term, buf->s, buf->len, false);
+            strbuf_free(buf);
+        }
+        term->paste_pos += n;
+
+        if (term->paste_pos < term->paste_len) {
+            queue_toplevel_callback(term_paste_callback, term);
+            return;
+        }
+    }
+    term_bracketed_paste_stop(term);
+    sfree(term->paste_buffer);
+    term->paste_buffer = NULL;
+    term->paste_len = 0;
+}
+
+/*
+ * Specialist string compare function. Returns true if the buffer of
+ * alen wide characters starting at a has as a prefix the buffer of
+ * blen characters starting at b.
+ */
+static bool wstartswith(const wchar_t *a, size_t alen,
+                        const wchar_t *b, size_t blen)
+{
+    return alen >= blen && !wcsncmp(a, b, blen);
+}
+
+void term_do_paste(Terminal *term, const wchar_t *data, int len)
+{
+    const wchar_t *p;
+    bool paste_controls = conf_get_bool(term->conf, CONF_paste_controls);
+
+    /*
+     * Pasting data into the terminal counts as a keyboard event (for
+     * purposes of the 'Reset scrollback on keypress' config option),
+     * unless the paste is zero-length.
+     */
+    if (len == 0)
+        return;
+    term_seen_key_event(term);
+
+    if (term->paste_buffer)
+        sfree(term->paste_buffer);
+    term->paste_pos = term->paste_len = 0;
+    term->paste_buffer = snewn(len + 12, wchar_t);
+
+    if (term->bracketed_paste)
+        term_bracketed_paste_start(term);
+
+    p = data;
+    while (p < data + len) {
+        wchar_t wc = *p++;
+
+        if (wc == sel_nl[0] &&
+            wstartswith(p-1, data+len-(p-1), sel_nl, sel_nl_sz)) {
+            /*
+             * This is the (platform-dependent) sequence that the host
+             * OS uses to represent newlines in clipboard data.
+             * Normalise it to a press of CR.
+             */
+            p += sel_nl_sz - 1;
+            wc = '\015';
+        }
+
+        if ((wc & ~(wint_t)0x9F) == 0) {
+            /*
+             * This is a control code, either in the range 0x00-0x1F
+             * or 0x80-0x9F. We reject all of these in pastecontrols
+             * mode, except for a small set of permitted ones.
+             */
+            if (!paste_controls) {
+                /* In line with xterm 292, accepted control chars are:
+                 * CR, LF, tab, backspace. (And DEL, i.e. 0x7F, but
+                 * that's permitted by virtue of not matching the bit
+                 * mask that got us into this if statement, so we
+                 * don't have to permit it here. */
+                static const unsigned mask =
+                    (1<<13) | (1<<10) | (1<<9) | (1<<8);
+
+                if (wc > 15 || !((mask >> wc) & 1))
+                    continue;
+            }
+
+            if (wc == '\033' && term->bracketed_paste &&
+                wstartswith(p-1, data+len-(p-1), L"\033[201~", 6)) {
+                /*
+                 * Also, in bracketed-paste mode, reject the ESC
+                 * character that begins the end-of-paste sequence.
+                 */
+                continue;
+            }
+        }
+
+        term->paste_buffer[term->paste_len++] = wc;
+    }
+
+    /* Assume a small paste will be OK in one go. */
+    if (term->paste_len < 256) {
+        if (term->ldisc) {
+            strbuf *buf = term_input_data_from_unicode(
+                term, term->paste_buffer, term->paste_len);
+            term_keyinput_internal(term, buf->s, buf->len, false);
+            strbuf_free(buf);
+        }
+        if (term->paste_buffer)
+            sfree(term->paste_buffer);
+        term_bracketed_paste_stop(term);
+        term->paste_buffer = NULL;
+        term->paste_pos = term->paste_len = 0;
+    }
+
+    queue_toplevel_callback(term_paste_callback, term);
+}
+
+void term_mouse(Terminal *term, Mouse_Button braw, Mouse_Button bcooked,
+                Mouse_Action a, int x, int y, bool shift, bool ctrl, bool alt)
+{
+    pos selpoint;
+    termline *ldata;
+    bool raw_mouse = (term->xterm_mouse &&
+                      !term->no_mouse_rep &&
+                      !(term->mouse_override && shift));
+    int default_seltype;
+
+    // Don't do anything if mouse movement events weren't requested;
+    // Note: return early to avoid doing all of this code on every mouse move
+    // event only to throw it away.
+    if (a == MA_MOVE && (!raw_mouse || term->xterm_mouse < 3)) {
+        return;
+    }
+
+    if (y < 0) {
+        y = 0;
+        if (a == MA_DRAG && !raw_mouse)
+            term_scroll(term, 0, -1);
+    }
+    if (y >= term->rows) {
+        y = term->rows - 1;
+        if (a == MA_DRAG && !raw_mouse)
+            term_scroll(term, 0, +1);
+    }
+    if (x < 0) {
+        if (y > 0 && !raw_mouse && term->seltype != RECTANGULAR) {
+            /*
+             * When we're using the mouse for normal raster-based
+             * selection, dragging off the left edge of a terminal row
+             * is treated the same as the right-hand end of the
+             * previous row, in that it's considered to identify a
+             * point _before_ the first character on row y.
+             *
+             * But if the mouse action is going to be used for
+             * anything else - rectangular selection, or xterm mouse
+             * tracking - then we disable this special treatment.
+             */
+            x = term->cols - 1;
+            y--;
+        } else
+            x = 0;
+    }
+    if (x >= term->cols)
+        x = term->cols - 1;
+
+    selpoint.y = y + term->disptop;
+    ldata = lineptr(selpoint.y);
+
+    if ((ldata->lattr & LATTR_MODE) != LATTR_NORM)
+        x /= 2;
+
+    /*
+     * Transform x through the bidi algorithm to find the _logical_
+     * click point from the physical one.
+     */
+    if (term_bidi_line(term, ldata, y) != NULL) {
+        x = term->post_bidi_cache[y].backward[x];
+    }
+
+    selpoint.x = x;
+    unlineptr(ldata);
+
+    /*
+     * If we're in the middle of a selection operation, we ignore raw
+     * mouse mode until it's done (we must have been not in raw mouse
+     * mode when it started).
+     * This makes use of Shift for selection reliable, and avoids the
+     * host seeing mouse releases for which they never saw corresponding
+     * presses.
+     */
+    if (raw_mouse &&
+        (term->selstate != ABOUT_TO) && (term->selstate != DRAGGING)) {
+        int encstate = 0, r, c;
+        bool wheel;
+        char abuf[32];
+        int len = 0;
+
+        if (term->ldisc) {
+
+            switch (braw) {
+              case MBT_LEFT:
+                encstate = 0x00;               /* left button down */
+                wheel = false;
+                break;
+              case MBT_MIDDLE:
+                encstate = 0x01;
+                wheel = false;
+                break;
+              case MBT_RIGHT:
+                encstate = 0x02;
+                wheel = false;
+                break;
+              case MBT_WHEEL_UP:
+                encstate = 0x40;
+                wheel = true;
+                break;
+              case MBT_WHEEL_DOWN:
+                encstate = 0x41;
+                wheel = true;
+                break;
+              case MBT_WHEEL_LEFT:
+                encstate = 0x42;
+                wheel = true;
+                break;
+              case MBT_WHEEL_RIGHT:
+                encstate = 0x43;
+                wheel = true;
+                break;
+              case MBT_NOTHING:
+                assert( a == MA_MOVE );
+                encstate = 0x03; // release; no buttons pressed
+                wheel = false;
+                break;
+              default:
+                return;
+            }
+            if (wheel) {
+                /* For mouse wheel buttons, we only ever expect to see
+                 * MA_CLICK actions, and we don't try to keep track of
+                 * the buttons being 'pressed' (since without matching
+                 * click/release pairs that's pointless). */
+                if (a != MA_CLICK)
+                    return;
+            } else switch (a) {
+              case MA_DRAG:
+                if (term->xterm_mouse == 1)
+                    return;
+                encstate += 0x20; // motion indicator
+                break;
+              case MA_MOVE:    // mouse move without buttons
+                assert( braw == MBT_NOTHING && bcooked == MBT_NOTHING  );
+                if (term->xterm_mouse < 3)
+                    return;
+
+                if (selpoint.x == term->raw_mouse_reported_x &&
+                    selpoint.y == term->raw_mouse_reported_y)
+                    return;
+
+                term->raw_mouse_reported_x = x;
+                term->raw_mouse_reported_y = y;
+
+                encstate += 0x20; // motion indicator
+                break;
+              case MA_RELEASE:
+                /* If multiple extensions are enabled, the xterm 1006 is used, so it's okay to check for only that */
+                if (!term->xterm_extended_mouse)
+                    encstate = 0x03;
+                term->mouse_is_down = 0;
+                break;
+              case MA_CLICK:
+                if (term->mouse_is_down == braw)
+                    return;
+                term->mouse_is_down = braw;
+                break;
+              default:
+                return;
+            }
+            if (shift)
+                encstate += 0x04;
+            if (ctrl)
+                encstate += 0x10;
+            r = y + 1;
+            c = x + 1;
+
+            /* Check the extensions in decreasing order of preference. Encoding the release event above assumes that 1006 comes first. */
+            if (term->xterm_extended_mouse) {
+                len = sprintf(abuf, "\033[<%d;%d;%d%c", encstate, c, r, a == MA_RELEASE ? 'm' : 'M');
+            } else if (term->urxvt_extended_mouse) {
+                len = sprintf(abuf, "\033[%d;%d;%dM", encstate + 32, c, r);
+            } else if (c <= 223 && r <= 223) {
+                len = sprintf(abuf, "\033[M%c%c%c", encstate + 32, c + 32, r + 32);
+            }
+            if (len > 0)
+                ldisc_send(term->ldisc, abuf, len, false);
+        }
+        return;
+    }
+
+    /*
+     * Set the selection type (rectangular or normal) at the start
+     * of a selection attempt, from the state of Alt.
+     */
+    if (!alt ^ !term->rect_select)
+        default_seltype = RECTANGULAR;
+    else
+        default_seltype = LEXICOGRAPHIC;
+
+    if (term->selstate == NO_SELECTION) {
+        term->seltype = default_seltype;
+    }
+
+    if (bcooked == MBT_SELECT && a == MA_CLICK) {
+        deselect(term);
+        term->selstate = ABOUT_TO;
+        term->seltype = default_seltype;
+        term->selanchor = selpoint;
+        term->selmode = SM_CHAR;
+    } else if (bcooked == MBT_SELECT && (a == MA_2CLK || a == MA_3CLK)) {
+        deselect(term);
+        term->selmode = (a == MA_2CLK ? SM_WORD : SM_LINE);
+        term->selstate = DRAGGING;
+        term->selstart = term->selanchor = selpoint;
+        term->selend = term->selstart;
+        incpos(term->selend);
+        sel_spread(term);
+    } else if ((bcooked == MBT_SELECT && a == MA_DRAG) ||
+               (bcooked == MBT_EXTEND && a != MA_RELEASE)) {
+        if (a == MA_DRAG &&
+            (term->selstate == NO_SELECTION || term->selstate == SELECTED)) {
+            /*
+             * This can happen if a front end has passed us a MA_DRAG
+             * without a prior MA_CLICK. OS X GTK does so, for
+             * example, if the initial button press was eaten by the
+             * WM when it activated the window in the first place. The
+             * nicest thing to do in this situation is to ignore
+             * further drags, and wait for the user to click in the
+             * window again properly if they want to select.
+             */
+            return;
+        }
+        if (term->selstate == ABOUT_TO && poseq(term->selanchor, selpoint))
+            return;
+        if (bcooked == MBT_EXTEND && a != MA_DRAG &&
+            term->selstate == SELECTED) {
+            if (term->seltype == LEXICOGRAPHIC) {
+                /*
+                 * For normal selection, we extend by moving
+                 * whichever end of the current selection is closer
+                 * to the mouse.
+                 */
+                if (posdiff(selpoint, term->selstart) <
+                    posdiff(term->selend, term->selstart) / 2) {
+                    term->selanchor = term->selend;
+                    decpos(term->selanchor);
+                } else {
+                    term->selanchor = term->selstart;
+                }
+            } else {
+                /*
+                 * For rectangular selection, we have a choice of
+                 * _four_ places to put selanchor and selpoint: the
+                 * four corners of the selection.
+                 */
+                if (2*selpoint.x < term->selstart.x + term->selend.x)
+                    term->selanchor.x = term->selend.x-1;
+                else
+                    term->selanchor.x = term->selstart.x;
+
+                if (2*selpoint.y < term->selstart.y + term->selend.y)
+                    term->selanchor.y = term->selend.y;
+                else
+                    term->selanchor.y = term->selstart.y;
+            }
+            term->selstate = DRAGGING;
+        }
+        if (term->selstate != ABOUT_TO && term->selstate != DRAGGING)
+            term->selanchor = selpoint;
+        term->selstate = DRAGGING;
+        if (term->seltype == LEXICOGRAPHIC) {
+            /*
+             * For normal selection, we set (selstart,selend) to
+             * (selpoint,selanchor) in some order.
+             */
+            if (poslt(selpoint, term->selanchor)) {
+                term->selstart = selpoint;
+                term->selend = term->selanchor;
+                incpos(term->selend);
+            } else {
+                term->selstart = term->selanchor;
+                term->selend = selpoint;
+                incpos(term->selend);
+            }
+        } else {
+            /*
+             * For rectangular selection, we may need to
+             * interchange x and y coordinates (if the user has
+             * dragged in the -x and +y directions, or vice versa).
+             */
+            term->selstart.x = min(term->selanchor.x, selpoint.x);
+            term->selend.x = 1+max(term->selanchor.x, selpoint.x);
+            term->selstart.y = min(term->selanchor.y, selpoint.y);
+            term->selend.y =   max(term->selanchor.y, selpoint.y);
+        }
+        sel_spread(term);
+    } else if ((bcooked == MBT_SELECT || bcooked == MBT_EXTEND) &&
+               a == MA_RELEASE) {
+        if (term->selstate == DRAGGING) {
+            /*
+             * We've completed a selection. We now transfer the
+             * data to the clipboard.
+             */
+            clipme(term, term->selstart, term->selend,
+                   (term->seltype == RECTANGULAR), false,
+                   term->mouse_select_clipboards,
+                   term->n_mouse_select_clipboards);
+            term->selstate = SELECTED;
+        } else
+            term->selstate = NO_SELECTION;
+    } else if (bcooked == MBT_PASTE
+               && (a == MA_CLICK
+#if MULTICLICK_ONLY_EVENT
+                   || a == MA_2CLK || a == MA_3CLK
+#endif
+                   )) {
+        term_request_paste(term, term->mouse_paste_clipboard);
+    }
+
+    /*
+     * Since terminal output is suppressed during drag-selects, we
+     * should make sure to write any pending output if one has just
+     * finished.
+     */
+    term_out(term, false);
+    term_schedule_update(term);
+}
+
+void term_cancel_selection_drag(Terminal *term)
+{
+    /*
+     * In unusual circumstances, a mouse drag might be interrupted by
+     * something that steals the rest of the mouse gesture. An example
+     * is the GTK popup menu appearing. In that situation, we'll never
+     * receive the MA_RELEASE that finishes the DRAGGING state, which
+     * means terminal output could be suppressed indefinitely. Call
+     * this function from the front end in such situations to restore
+     * sensibleness.
+     */
+    if (term->selstate == DRAGGING)
+        term->selstate = NO_SELECTION;
+    term_out(term, false);
+    term_schedule_update(term);
+}
+
+static int shift_bitmap(bool shift, bool ctrl, bool alt, bool *consumed_alt)
+{
+    int bitmap = (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0);
+    if (bitmap)
+        bitmap++;
+    if (alt && consumed_alt)
+        *consumed_alt = true;
+    return bitmap;
+}
+
+int format_arrow_key(char *buf, Terminal *term, int xkey,
+                     bool shift, bool ctrl, bool alt, bool *consumed_alt)
+{
+    char *p = buf;
+
+    if (term->vt52_mode)
+        p += sprintf(p, "\x1B%c", xkey);
+    else {
+        bool app_flg = (term->app_cursor_keys && !term->no_applic_c);
+#if 0
+        /*
+         * RDB: VT100 & VT102 manuals both state the app cursor
+         * keys only work if the app keypad is on.
+         *
+         * SGT: That may well be true, but xterm disagrees and so
+         * does at least one application, so I've #if'ed this out
+         * and the behaviour is back to PuTTY's original: app
+         * cursor and app keypad are independently switchable
+         * modes. If anyone complains about _this_ I'll have to
+         * put in a configurable option.
+         */
+        if (!term->app_keypad_keys)
+            app_flg = 0;
+#endif
+
+        int bitmap = 0;
+
+        /* Adjustment based on Shift, Ctrl and/or Alt */
+        switch (term->sharrow_type) {
+          case SHARROW_APPLICATION:
+            if (ctrl)
+                app_flg = !app_flg;
+            break;
+          case SHARROW_BITMAP:
+            bitmap = shift_bitmap(shift, ctrl, alt, consumed_alt);
+            break;
+        }
+
+        if (app_flg)
+            p += sprintf(p, "\x1BO%c", xkey);
+        else if (bitmap)
+            p += sprintf(p, "\x1B[1;%d%c", bitmap, xkey);
+        else
+            p += sprintf(p, "\x1B[%c", xkey);
+    }
+
+    return p - buf;
+}
+
+int format_function_key(char *buf, Terminal *term, int key_number,
+                        bool shift, bool ctrl, bool alt, bool *consumed_alt)
+{
+    char *p = buf;
+
+    static const int key_number_to_tilde_code[] = {
+        -1,                 /* no such key as F0 */
+        11, 12, 13, 14, 15, /*gap*/ 17, 18, 19, 20, 21, /*gap*/
+        23, 24, 25, 26, /*gap*/ 28, 29, /*gap*/ 31, 32, 33, 34,
+    };
+
+    assert(key_number > 0);
+    assert(key_number < lenof(key_number_to_tilde_code));
+
+    int index = key_number;
+    if (term->funky_type != FUNKY_XTERM_216 && term->funky_type != FUNKY_SCO) {
+        if (shift && index <= 10) {
+            shift = false;
+            index += 10;
+        }
+    }
+
+    int code = key_number_to_tilde_code[index];
+
+    if (term->funky_type == FUNKY_SCO) {
+        /* SCO function keys */
+        static const char sco_codes[] =
+            "MNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz@[\\]^_`{";
+        index = (key_number >= 1 && key_number <= 12) ? key_number - 1 : 0;
+        if (shift) index += 12;
+        if (ctrl) index += 24;
+        p += sprintf(p, "\x1B[%c", sco_codes[index]);
+    } else if ((term->vt52_mode || term->funky_type == FUNKY_VT100P) &&
+               code >= 11 && code <= 24) {
+        int offt = 0;
+        if (code > 15)
+            offt++;
+        if (code > 21)
+            offt++;
+        if (term->vt52_mode)
+            p += sprintf(p, "\x1B%c", code + 'P' - 11 - offt);
+        else
+            p += sprintf(p, "\x1BO%c", code + 'P' - 11 - offt);
+    } else if (term->funky_type == FUNKY_LINUX && code >= 11 && code <= 15) {
+        p += sprintf(p, "\x1B[[%c", code + 'A' - 11);
+    } else if ((term->funky_type == FUNKY_XTERM ||
+                term->funky_type == FUNKY_XTERM_216) &&
+               code >= 11 && code <= 14) {
+        if (term->vt52_mode)
+            p += sprintf(p, "\x1B%c", code + 'P' - 11);
+        else {
+            int bitmap = 0;
+            if (term->funky_type == FUNKY_XTERM_216)
+                bitmap = shift_bitmap(shift, ctrl, alt, consumed_alt);
+            if (bitmap)
+                p += sprintf(p, "\x1B[1;%d%c", bitmap, code + 'P' - 11);
+            else
+                p += sprintf(p, "\x1BO%c", code + 'P' - 11);
+        }
+    } else {
+        int bitmap = 0;
+        if (term->funky_type == FUNKY_XTERM_216)
+            bitmap = shift_bitmap(shift, ctrl, alt, consumed_alt);
+        if (bitmap)
+            p += sprintf(p, "\x1B[%d;%d~", code, bitmap);
+        else
+            p += sprintf(p, "\x1B[%d~", code);
+    }
+
+    return p - buf;
+}
+
+int format_small_keypad_key(char *buf, Terminal *term, SmallKeypadKey key,
+                            bool shift, bool ctrl, bool alt,
+                            bool *consumed_alt)
+{
+    char *p = buf;
+
+    int code;
+    switch (key) {
+      case SKK_HOME: code = 1; break;
+      case SKK_INSERT: code = 2; break;
+      case SKK_DELETE: code = 3; break;
+      case SKK_END: code = 4; break;
+      case SKK_PGUP: code = 5; break;
+      case SKK_PGDN: code = 6; break;
+      default: unreachable("bad small keypad key enum value");
+    }
+
+    /* Reorder edit keys to physical order */
+    if (term->funky_type == FUNKY_VT400 && code <= 6)
+        code = "\0\2\1\4\5\3\6"[code];
+
+    if (term->vt52_mode && code > 0 && code <= 6) {
+        p += sprintf(p, "\x1B%c", " HLMEIG"[code]);
+    } else if (term->funky_type == FUNKY_SCO) {
+        static const char codes[] = "HL.FIG";
+        if (code == 3) {
+            *p++ = '\x7F';
+        } else {
+            p += sprintf(p, "\x1B[%c", codes[code-1]);
+        }
+    } else if ((code == 1 || code == 4) && term->rxvt_homeend) {
+        p += sprintf(p, code == 1 ? "\x1B[H" : "\x1BOw");
+    } else {
+        if (term->vt52_mode) {
+	    p += sprintf(p, "\x1B[%d~", code);
+        } else {
+            int bitmap = 0;
+            if (term->funky_type == FUNKY_XTERM_216)
+                bitmap = shift_bitmap(shift, ctrl, alt, consumed_alt);
+            if (bitmap)
+                p += sprintf(p, "\x1B[%d;%d~", code, bitmap);
+            else
+                p += sprintf(p, "\x1B[%d~", code);
+        }
+    }
+
+    return p - buf;
+}
+
+int format_numeric_keypad_key(char *buf, Terminal *term, char key,
+                              bool shift, bool ctrl)
+{
+    char *p = buf;
+    bool app_keypad = (term->app_keypad_keys && !term->no_applic_k);
+
+    if (term->nethack_keypad && (key >= '1' && key <= '9')) {
+        static const char nh_base[] = "bjnh.lyku";
+        char c = nh_base[key - '1'];
+        if (ctrl && c != '.')
+            c &= 0x1F;
+        else if (shift && c != '.')
+            c += 'A'-'a';
+        *p++ = c;
+    } else {
+        int xkey = 0;
+
+        if (term->funky_type == FUNKY_VT400 ||
+            (term->funky_type <= FUNKY_LINUX && app_keypad)) {
+            switch (key) {
+              case 'G': xkey = 'P'; break;
+              case '/': xkey = 'Q'; break;
+              case '*': xkey = 'R'; break;
+              case '-': xkey = 'S'; break;
+            }
+        }
+
+        if (app_keypad) {
+            switch (key) {
+              case '0': xkey = 'p'; break;
+              case '1': xkey = 'q'; break;
+              case '2': xkey = 'r'; break;
+              case '3': xkey = 's'; break;
+              case '4': xkey = 't'; break;
+              case '5': xkey = 'u'; break;
+              case '6': xkey = 'v'; break;
+              case '7': xkey = 'w'; break;
+              case '8': xkey = 'x'; break;
+              case '9': xkey = 'y'; break;
+              case '.': xkey = 'n'; break;
+              case '\r': xkey = 'M'; break;
+
+              case '+':
+                /*
+                 * Keypad + is tricky. It covers a space that would
+                 * be taken up on the VT100 by _two_ keys; so we
+                 * let Shift select between the two. Worse still,
+                 * in xterm function key mode we change which two...
+                 */
+                if (term->funky_type == FUNKY_XTERM)
+                    xkey = shift ? 'l' : 'k';
+                else
+                    xkey = shift ? 'm' : 'l';
+                break;
+
+              case '/':
+                if (term->funky_type == FUNKY_XTERM)
+                    xkey = 'o';
+                break;
+              case '*':
+                if (term->funky_type == FUNKY_XTERM)
+                    xkey = 'j';
+                break;
+              case '-':
+                if (term->funky_type == FUNKY_XTERM)
+                    xkey = 'm';
+                break;
+            }
+        }
+
+        if (xkey) {
+            if (term->vt52_mode) {
+                if (xkey >= 'P' && xkey <= 'S')
+                    p += sprintf(p, "\x1B%c", xkey);
+                else
+                    p += sprintf(p, "\x1B?%c", xkey);
+            } else
+                p += sprintf(p, "\x1BO%c", xkey);
+        }
+    }
+
+    return p - buf;
+}
+
+void term_keyinputw(Terminal *term, const wchar_t *widebuf, int len)
+{
+    strbuf *buf = term_input_data_from_unicode(term, widebuf, len);
+    if (buf->len)
+        term_keyinput_internal(term, buf->s, buf->len, true);
+    strbuf_free(buf);
+}
+
+void term_keyinput(Terminal *term, int codepage, const void *str, int len)
+{
+    if (codepage < 0 || codepage == term->ucsdata->line_codepage) {
+        /*
+         * This text needs no translation, either because it's already
+         * in the right character set, or because we got the special
+         * codepage value -1 from our caller which means 'this data
+         * should be charset-agnostic, just send it raw' (for really
+         * simple things like control characters).
+         */
+        term_keyinput_internal(term, str, len, true);
+    } else {
+        strbuf *buf = term_input_data_from_charset(term, codepage, str, len);
+        if (buf->len)
+            term_keyinput_internal(term, buf->s, buf->len, true);
+        strbuf_free(buf);
+    }
+}
+
+void term_nopaste(Terminal *term)
+{
+    if (term->paste_len == 0)
+        return;
+    sfree(term->paste_buffer);
+    term_bracketed_paste_stop(term);
+    term->paste_buffer = NULL;
+    term->paste_len = 0;
+}
+
+static void deselect(Terminal *term)
+{
+    term->selstate = NO_SELECTION;
+    term->selstart.x = term->selstart.y = term->selend.x = term->selend.y = 0;
+}
+
+void term_lost_clipboard_ownership(Terminal *term, int clipboard)
+{
+    if (!(term->n_mouse_select_clipboards > 1 &&
+          clipboard == term->mouse_select_clipboards[1]))
+        return;
+
+    deselect(term);
+    term_update(term);
+
+    /*
+     * Since terminal output is suppressed during drag-selects, we
+     * should make sure to write any pending output if one has just
+     * finished.
+     */
+    term_out(term, false);
+}
+
+static void term_added_data(Terminal *term, bool called_from_term_data)
+{
+    if (!term->in_term_out) {
+        term->in_term_out = true;
+        term_out(term, called_from_term_data);
+        term->in_term_out = false;
+    }
+}
+
+size_t term_data(Terminal *term, const void *data, size_t len)
+{
+    bufchain_add(&term->inbuf, data, len);
+    term_added_data(term, true);
+    return bufchain_size(&term->inbuf);
+}
+
+void term_provide_logctx(Terminal *term, LogContext *logctx)
+{
+    term->logctx = logctx;
+}
+
+void term_set_focus(Terminal *term, bool has_focus)
+{
+    term->has_focus = has_focus;
+    term_schedule_cblink(term);
+}
+
+/*
+ * Provide "auto" settings for remote tty modes, suitable for an
+ * application with a terminal window.
+ */
+char *term_get_ttymode(Terminal *term, const char *mode)
+{
+    const char *val = NULL;
+    if (strcmp(mode, "ERASE") == 0) {
+        val = term->bksp_is_delete ? "^?" : "^H";
+    } else if (strcmp(mode, "IUTF8") == 0) {
+        val = (term->ucsdata->line_codepage == CP_UTF8) ? "yes" : "no";
+    }
+    /* FIXME: perhaps we should set ONLCR based on lfhascr as well? */
+    /* FIXME: or ECHO and friends based on local echo state? */
+    return dupstr(val);
+}
+
+struct term_userpass_state {
+    size_t curr_prompt;
+    bool done_prompt;   /* printed out prompt yet? */
+};
+
+/* Tiny wrapper to make it easier to write lots of little strings */
+static inline void term_write(Terminal *term, ptrlen data)
+{
+    term_data(term, data.ptr, data.len);
+}
+
+/*
+ * Signal that a prompts_t is done. This involves sending a
+ * notification to the caller, and also turning off our own callback
+ * that listens for more data arriving in the ldisc's input queue.
+ */
+static inline SeatPromptResult signal_prompts_t(Terminal *term, prompts_t *p,
+                                                SeatPromptResult spr)
+{
+    assert(p->callback && "Asynchronous userpass input requires a callback");
+    queue_toplevel_callback(p->callback, p->callback_ctx);
+    if (term->ldisc)
+        ldisc_enable_prompt_callback(term->ldisc, NULL);
+    p->spr = spr;
+    return spr;
+}
+
+/*
+ * Process some terminal data in the course of username/password
+ * input.
+ */
+SeatPromptResult term_get_userpass_input(Terminal *term, prompts_t *p)
+{
+    if (!term->ldisc) {
+        /* Can't handle interactive prompts without an ldisc */
+        return signal_prompts_t(term, p, SPR_SW_ABORT(
+            "Terminal not prepared for interactive prompts"));
+    }
+
+    if (p->spr.kind != SPRK_INCOMPLETE) {
+        /* We've already finished these prompts, so return the same
+         * result again */
+        return p->spr;
+    }
+
+    struct term_userpass_state *s = (struct term_userpass_state *)p->data;
+
+    if (!s) {
+        /*
+         * First call. Set some stuff up.
+         */
+        p->data = s = snew(struct term_userpass_state);
+        p->spr = SPR_INCOMPLETE;
+        s->curr_prompt = 0;
+        s->done_prompt = false;
+        /* We only print the `name' caption if we have to... */
+        if (p->name_reqd && p->name) {
+            ptrlen plname = ptrlen_from_asciz(p->name);
+            term_write(term, plname);
+            if (!ptrlen_endswith(plname, PTRLEN_LITERAL("\n"), NULL))
+                term_write(term, PTRLEN_LITERAL("\r\n"));
+        }
+        /* ...but we always print any `instruction'. */
+        if (p->instruction) {
+            ptrlen plinst = ptrlen_from_asciz(p->instruction);
+            term_write(term, plinst);
+            if (!ptrlen_endswith(plinst, PTRLEN_LITERAL("\n"), NULL))
+                term_write(term, PTRLEN_LITERAL("\r\n"));
+        }
+        /*
+         * Zero all the results, in case we abort half-way through.
+         */
+        {
+            int i;
+            for (i = 0; i < (int)p->n_prompts; i++)
+                prompt_set_result(p->prompts[i], "");
+        }
+    }
+
+    while (s->curr_prompt < p->n_prompts) {
+
+        prompt_t *pr = p->prompts[s->curr_prompt];
+        bool finished_prompt = false;
+
+        if (!s->done_prompt) {
+            term_write(term, ptrlen_from_asciz(pr->prompt));
+            s->done_prompt = true;
+        }
+
+        /* Breaking out here ensures that the prompt is printed even
+         * if we're now waiting for user data. */
+        if (!ldisc_has_input_buffered(term->ldisc))
+            break;
+
+        /* FIXME: should we be using local-line-editing code instead? */
+        while (!finished_prompt && ldisc_has_input_buffered(term->ldisc)) {
+            LdiscInputToken tok = ldisc_get_input_token(term->ldisc);
+
+            char c;
+            if (tok.is_special) {
+                switch (tok.code) {
+                  case SS_EOL: c = 13; break;
+                  case SS_EC: c = 8; break;
+                  case SS_IP: c = 3; break;
+                  case SS_EOF: c = 3; break;
+                  default: continue;
+                }
+            } else {
+                c = tok.chr;
+            }
+
+            switch (c) {
+              case 10:
+              case 13:
+                term_write(term, PTRLEN_LITERAL("\r\n"));
+                /* go to next prompt, if any */
+                s->curr_prompt++;
+                s->done_prompt = false;
+                finished_prompt = true; /* break out */
+                break;
+              case 8:
+              case 127:
+                if (pr->result->len > 0) {
+                    if (pr->echo)
+                        term_write(term, PTRLEN_LITERAL("\b \b"));
+                    strbuf_shrink_by(pr->result, 1);
+                }
+                break;
+              case 21:
+              case 27:
+                while (pr->result->len > 0) {
+                    if (pr->echo)
+                        term_write(term, PTRLEN_LITERAL("\b \b"));
+                    strbuf_shrink_by(pr->result, 1);
+                }
+                break;
+              case 3:
+              case 4:
+                /* Immediate abort. */
+                term_write(term, PTRLEN_LITERAL("\r\n"));
+                sfree(s);
+                p->data = NULL;
+                return signal_prompts_t(term, p, SPR_USER_ABORT);
+              default:
+                /*
+                 * This simplistic check for printability is disabled
+                 * when we're doing password input, because some people
+                 * have control characters in their passwords.
+                 */
+                if (!pr->echo || (c >= ' ' && c <= '~') ||
+                     ((unsigned char) c >= 160)) {
+                    put_byte(pr->result, c);
+                    if (pr->echo)
+                        term_write(term, make_ptrlen(&c, 1));
+                }
+                break;
+            }
+        }
+
+    }
+
+    if (s->curr_prompt < p->n_prompts) {
+        ldisc_enable_prompt_callback(term->ldisc, p);
+        return SPR_INCOMPLETE;
+    } else {
+        sfree(s);
+        p->data = NULL;
+        return signal_prompts_t(term, p, SPR_OK);
+    }
+}
+
+void term_notify_minimised(Terminal *term, bool minimised)
+{
+    term->minimised = minimised;
+}
+
+void term_notify_palette_changed(Terminal *term)
+{
+    palette_reset(term, true);
+}
+
+void term_notify_window_pos(Terminal *term, int x, int y)
+{
+    term->winpos_x = x;
+    term->winpos_y = y;
+}
+
+void term_notify_window_size_pixels(Terminal *term, int x, int y)
+{
+    term->winpixsize_x = x;
+    term->winpixsize_y = y;
+}
diff --git putty-0.81/terminal/terminal.h putty-url-0.81/terminal/terminal.h
index e036131..a65387b 100644
--- putty-0.81/terminal/terminal.h
+++ putty-url-0.81/terminal/terminal.h
@@ -429,6 +429,8 @@ struct terminal_tag {
         WIN_RESIZE_NO, WIN_RESIZE_NEED_SEND, WIN_RESIZE_AWAIT_REPLY
     } win_resize_pending;
     int win_resize_pending_w, win_resize_pending_h;
+
+    int url_update; /* HACK: PuTTY-url */
 };
 
 static inline bool in_utf(Terminal *term)
diff --git putty-url-0.81/terminal/terminal.h.orig putty-url-0.81/terminal/terminal.h.orig
new file mode 100644
index 0000000..8744185
--- /dev/null
+++ putty-url-0.81/terminal/terminal.h.orig
@@ -0,0 +1,565 @@
+/*
+ * Internals of the Terminal structure, for those other modules
+ * which need to look inside it. It would be nice if this could be
+ * folded back into terminal.c in future, with an abstraction layer
+ * to handle everything that other modules need to know about it;
+ * but for the moment, this will do.
+ */
+
+#ifndef PUTTY_TERMINAL_H
+#define PUTTY_TERMINAL_H
+
+#include "tree234.h"
+
+struct beeptime {
+    struct beeptime *next;
+    unsigned long ticks;
+};
+
+#define TRUST_SIGIL_WIDTH 3
+#define TRUST_SIGIL_CHAR 0xDFFE
+
+typedef struct {
+    int y, x;
+} pos;
+
+typedef struct termchar termchar;
+typedef struct termline termline;
+
+struct termchar {
+    /*
+     * Any code in terminal.c which definitely needs to be changed
+     * when extra fields are added here is labelled with a comment
+     * saying FULL-TERMCHAR.
+     */
+    unsigned long chr;
+    unsigned long attr;
+    truecolour truecolour;
+
+    /*
+     * The cc_next field is used to link multiple termchars
+     * together into a list, so as to fit more than one character
+     * into a character cell (Unicode combining characters).
+     *
+     * cc_next is a relative offset into the current array of
+     * termchars. I.e. to advance to the next character in a list,
+     * one does `tc += tc->next'.
+     *
+     * Zero means end of list.
+     */
+    int cc_next;
+};
+
+struct termline {
+    unsigned short lattr;
+    int cols;                          /* number of real columns on the line */
+    int size;                          /* number of allocated termchars
+                                        * (cc-lists may make this > cols) */
+    bool temporary;                    /* true if decompressed from scrollback */
+    int cc_free;                       /* offset to first cc in free list */
+    struct termchar *chars;
+    bool trusted;
+};
+
+struct bidi_cache_entry {
+    int width;
+    bool trusted;
+    struct termchar *chars;
+    int *forward, *backward;           /* the permutations of line positions */
+};
+
+struct term_utf8_decode {
+    int state;                         /* Is there a pending UTF-8 character */
+    int chr;                           /* and what is it so far? */
+    int size;                          /* The size of the UTF character. */
+};
+
+struct terminal_tag {
+
+    int compatibility_level;
+
+    tree234 *scrollback;               /* lines scrolled off top of screen */
+    tree234 *screen;                   /* lines on primary screen */
+    tree234 *alt_screen;               /* lines on alternate screen */
+    int disptop;                       /* distance scrolled back (0 or -ve) */
+    int tempsblines;                   /* number of lines of .scrollback that
+                                          can be retrieved onto the terminal
+                                          ("temporary scrollback") */
+
+    termline **disptext;               /* buffer of text on real screen */
+    int dispcursx, dispcursy;          /* location of cursor on real screen */
+    int curstype;                      /* type of cursor on real screen */
+
+#define VBELL_TIMEOUT (TICKSPERSEC/10) /* visual bell lasts 1/10 sec */
+
+    struct beeptime *beephead, *beeptail;
+    int nbeeps;
+    bool beep_overloaded;
+    long lastbeep;
+
+#define TTYPE termchar
+#define TSIZE (sizeof(TTYPE))
+
+    int default_attr, curr_attr, save_attr;
+    truecolour curr_truecolour, save_truecolour;
+    termchar basic_erase_char, erase_char;
+
+    bufchain inbuf;                    /* terminal input buffer */
+
+    pos curs;                          /* cursor */
+    pos savecurs;                      /* saved cursor position */
+    int marg_t, marg_b;                /* scroll margins */
+    bool dec_om;                       /* DEC origin mode flag */
+    bool wrap, wrapnext;               /* wrap flags */
+    bool insert;                       /* insert-mode flag */
+    int cset;                          /* 0 or 1: which char set */
+    int save_cset, save_csattr;        /* saved with cursor position */
+    bool save_utf, save_wnext;         /* saved with cursor position */
+    bool rvideo;                       /* global reverse video flag */
+    unsigned long rvbell_startpoint;   /* for ESC[?5hESC[?5l vbell */
+    bool cursor_on;                    /* cursor enabled flag */
+    bool reset_132;                    /* Flag ESC c resets to 80 cols */
+    bool use_bce;                      /* Use Background coloured erase */
+    bool cblinker;                     /* When blinking is the cursor on ? */
+    bool tblinker;                     /* When the blinking text is on */
+    bool blink_is_real;                /* Actually blink blinking text */
+    int sco_acs, save_sco_acs;         /* CSI 10,11,12m -> OEM charset */
+    bool vt52_bold;                    /* Force bold on non-bold colours */
+    bool utf;                          /* Are we in toggleable UTF-8 mode? */
+    term_utf8_decode utf8;             /* If so, here's our decoding state */
+    bool printing, only_printing;      /* Are we doing ANSI printing? */
+    int print_state;                   /* state of print-end-sequence scan */
+    bufchain printer_buf;              /* buffered data for printer */
+    printer_job *print_job;
+
+    /* ESC 7 saved state for the alternate screen */
+    pos alt_savecurs;
+    int alt_save_attr;
+    truecolour alt_save_truecolour;
+    int alt_save_cset, alt_save_csattr;
+    bool alt_save_utf;
+    bool alt_save_wnext;
+    int alt_save_sco_acs;
+
+    int rows, cols, savelines;
+    bool has_focus;
+    bool in_vbell;
+    long vbell_end;
+    bool app_cursor_keys, app_keypad_keys, vt52_mode;
+    bool repeat_off, srm_echo, cr_lf_return;
+    bool seen_disp_event;
+    bool big_cursor;
+
+    bool xterm_mouse_forbidden;
+    int xterm_mouse;                   /* send mouse messages to host */
+    bool xterm_extended_mouse;
+    bool urxvt_extended_mouse;
+    int mouse_is_down;                 /* used while tracking mouse buttons */
+    int raw_mouse_reported_x;
+    int raw_mouse_reported_y;
+
+    bool bracketed_paste, bracketed_paste_active;
+
+    int cset_attr[2];
+
+/*
+ * Saved settings on the alternate screen.
+ */
+    int alt_x, alt_y;
+    bool alt_wnext, alt_ins;
+    bool alt_om, alt_wrap;
+    int alt_cset, alt_sco_acs;
+    bool alt_utf;
+    int alt_t, alt_b;
+    int alt_which;
+    int alt_sblines; /* # of lines on alternate screen that should be used for scrollback. */
+
+#define ARGS_MAX 32                    /* max # of esc sequence arguments */
+#define ARG_DEFAULT 0                  /* if an arg isn't specified */
+#define def(a,d) ( (a) == ARG_DEFAULT ? (d) : (a) )
+    unsigned esc_args[ARGS_MAX];
+    int esc_nargs;
+    int esc_query;
+#define ANSI(x,y)       ((x)+((y)*256))
+#define ANSI_QUE(x)     ANSI(x,1)
+
+#define OSC_STR_MAX 2048
+    int osc_strlen;
+    char osc_string[OSC_STR_MAX + 1];
+    bool osc_w;
+
+    char id_string[1024];
+
+    unsigned char *tabs;
+
+    enum {
+        TOPLEVEL,
+        SEEN_ESC,
+        SEEN_CSI,
+        SEEN_OSC,
+        SEEN_OSC_W,
+
+        DO_CTRLS,
+
+        SEEN_OSC_P,
+        OSC_STRING, OSC_MAYBE_ST, OSC_MAYBE_ST_UTF8,
+        VT52_ESC,
+        VT52_Y1,
+        VT52_Y2,
+        VT52_FG,
+        VT52_BG
+    } termstate;
+
+    enum {
+        NO_SELECTION, ABOUT_TO, DRAGGING, SELECTED
+    } selstate;
+    enum {
+        LEXICOGRAPHIC, RECTANGULAR
+    } seltype;
+    enum {
+        SM_CHAR, SM_WORD, SM_LINE
+    } selmode;
+    pos selstart, selend, selanchor;
+
+    short wordness[256];
+
+    /* Mask of attributes to pay attention to when painting. */
+    int attr_mask;
+
+    wchar_t *paste_buffer;
+    int paste_len, paste_pos;
+
+    Backend *backend;
+
+    Ldisc *ldisc;
+
+    TermWin *win;
+
+    LogContext *logctx;
+
+    struct unicode_data *ucsdata;
+
+    unsigned long last_graphic_char;
+
+    /*
+     * We maintain a full copy of a Conf here, not merely a pointer
+     * to it. That way, when we're passed a new one for
+     * reconfiguration, we can check the differences and adjust the
+     * _current_ setting of (e.g.) auto wrap mode rather than only
+     * the default.
+     */
+    Conf *conf;
+
+    /*
+     * GUI implementations of seat_output call term_out, but it can
+     * also be called from the ldisc if the ldisc is called _within_
+     * term_out. So we have to guard against re-entrancy - if
+     * seat_output is called recursively like this, it will simply add
+     * data to the end of the buffer term_out is in the process of
+     * working through.
+     */
+    bool in_term_out;
+
+    /*
+     * We don't permit window updates too close together, to avoid CPU
+     * churn pointlessly redrawing the window faster than the user can
+     * read. So after an update, we set window_update_cooldown = true
+     * and schedule a timer to reset it to false. In between those
+     * times, window updates are not performed, and instead we set
+     * window_update_pending = true, which will remind us to perform
+     * the deferred redraw when the cooldown period ends and
+     * window_update_cooldown is reset to false.
+     */
+    bool window_update_pending, window_update_cooldown;
+    long window_update_cooldown_end;
+
+    /*
+     * Track pending blinks and tblinks.
+     */
+    bool tblink_pending, cblink_pending;
+    long next_tblink, next_cblink;
+
+    /*
+     * These are buffers used by the bidi and Arabic shaping code.
+     */
+    termchar *ltemp;
+    int ltemp_size;
+    bidi_char *wcFrom, *wcTo;
+    int wcFromTo_size;
+    struct bidi_cache_entry *pre_bidi_cache, *post_bidi_cache;
+    size_t bidi_cache_size;
+
+    /*
+     * Current trust state, used to annotate every line of the
+     * terminal that a graphic character is output to.
+     */
+    bool trusted;
+
+    /*
+     * We copy a bunch of stuff out of the Conf structure into local
+     * fields in the Terminal structure, to avoid the repeated
+     * tree234 lookups which would be involved in fetching them from
+     * the former every time.
+     */
+    bool ansi_colour;
+    char *answerback;
+    int answerbacklen;
+    bool no_arabicshaping;
+    int beep;
+    bool bellovl;
+    int bellovl_n;
+    int bellovl_s;
+    int bellovl_t;
+    bool no_bidi;
+    bool bksp_is_delete;
+    bool blink_cur;
+    bool blinktext;
+    bool cjk_ambig_wide;
+    int conf_height;
+    int conf_width;
+    bool crhaslf;
+    bool erase_to_scrollback;
+    int funky_type, sharrow_type;
+    bool lfhascr;
+    bool logflush;
+    int logtype;
+    bool mouse_override;
+    bool nethack_keypad;
+    bool no_alt_screen;
+    bool no_applic_c;
+    bool no_applic_k;
+    bool no_dbackspace;
+    bool no_mouse_rep;
+    bool no_remote_charset;
+    bool no_remote_resize;
+    bool no_remote_wintitle;
+    bool no_remote_clearscroll;
+    bool rawcnp;
+    bool utf8linedraw;
+    bool rect_select;
+    int remote_qtitle_action;
+    bool rxvt_homeend;
+    bool scroll_on_disp;
+    bool scroll_on_key;
+    bool xterm_256_colour;
+    bool true_colour;
+
+    wchar_t *last_selected_text;
+    int *last_selected_attr;
+    truecolour *last_selected_tc;
+    size_t last_selected_len;
+    int mouse_select_clipboards[N_CLIPBOARDS];
+    int n_mouse_select_clipboards;
+    int mouse_paste_clipboard;
+
+    char *window_title, *icon_title;
+    int wintitle_codepage, icontitle_codepage;
+    bool minimised;
+
+    BidiContext *bidi_ctx;
+
+    /* Multi-layered colour palette. The colours from Conf (plus the
+     * default xterm-256 ones that don't have Conf ids at all) have
+     * lowest priority, followed by platform overrides if any,
+     * followed by escape-sequence overrides during the session. */
+    struct term_subpalette {
+        rgb values[OSC4_NCOLOURS];
+        bool present[OSC4_NCOLOURS];
+    } subpalettes[3];
+#define SUBPAL_CONF 0
+#define SUBPAL_PLATFORM 1
+#define SUBPAL_SESSION 2
+
+    /* The composite palette that we make out of the above */
+    rgb palette[OSC4_NCOLOURS];
+
+    unsigned winpos_x, winpos_y, winpixsize_x, winpixsize_y;
+
+    /*
+     * Assorted 'pending' flags for ancillary window changes performed
+     * in term_update. Generally, to trigger one of these operations,
+     * you set the pending flag and/or the parameters here, then call
+     * term_schedule_update.
+     */
+    bool win_move_pending;
+    int win_move_pending_x, win_move_pending_y;
+    bool win_zorder_pending;
+    bool win_zorder_top;
+    bool win_minimise_pending;
+    bool win_minimise_enable;
+    bool win_maximise_pending;
+    bool win_maximise_enable;
+    bool win_title_pending, win_icon_title_pending;
+    bool win_pointer_shape_pending;
+    bool win_pointer_shape_raw;
+    bool win_refresh_pending;
+    bool win_scrollbar_update_pending;
+    bool win_palette_pending;
+    unsigned win_palette_pending_min, win_palette_pending_limit;
+
+    /*
+     * Unlike the rest of the above 'pending' flags, the one for
+     * window resizing has to be more complicated, because it's very
+     * likely that a server sending a window-resize escape sequence is
+     * going to follow it up immediately with further terminal output
+     * that draws a full-screen application expecting the terminal to
+     * be the new size.
+     *
+     * So, once we've requested a window resize from the TermWin, we
+     * have to stop processing terminal data until we get back the
+     * notification that our window really has changed size (or until
+     * we find out that it's not going to).
+     *
+     * Hence, window resizes go through a small state machine with two
+     * different kinds of 'pending'. NEED_SEND is the state where
+     * we've received an escape sequence asking for a new size but not
+     * yet sent it to the TermWin via win_request_resize; AWAIT_REPLY
+     * is the state where we've sent it to the TermWin and are
+     * expecting a call back to term_size().
+     *
+     * So _both_ of those 'pending' states inhibit terminal output
+     * processing.
+     *
+     * (Hence, once we're in either state, we should never handle
+     * another resize sequence, so the only possible path through this
+     * state machine is to get all the way back to the ground state
+     * before doing anything else interesting.)
+     */
+    enum {
+        WIN_RESIZE_NO, WIN_RESIZE_NEED_SEND, WIN_RESIZE_AWAIT_REPLY
+    } win_resize_pending;
+    int win_resize_pending_w, win_resize_pending_h;
+};
+
+static inline bool in_utf(Terminal *term)
+{
+    return term->utf || term->ucsdata->line_codepage == CP_UTF8;
+}
+
+unsigned long term_translate(
+    Terminal *term, term_utf8_decode *utf8, unsigned char c);
+static inline int term_char_width(Terminal *term, unsigned int c)
+{
+    return term->cjk_ambig_wide ? mk_wcwidth_cjk(c) : mk_wcwidth(c);
+}
+
+/*
+ * UCSINCOMPLETE is returned from term_translate if it's successfully
+ * absorbed a byte but not emitted a complete character yet.
+ * UCSTRUNCATED indicates a truncated multibyte sequence (so the
+ * caller emits an error character and then calls term_translate again
+ * with the same input byte). UCSINVALID indicates some other invalid
+ * multibyte sequence, such as an overlong synonym, or a standalone
+ * continuation byte, or a completely illegal thing like 0xFE. These
+ * values are not stored in the terminal data structures at all.
+ */
+#define UCSINCOMPLETE 0x8000003FU    /* '?' */
+#define UCSTRUNCATED  0x80000021U    /* '!' */
+#define UCSINVALID    0x8000002AU    /* '*' */
+
+/*
+ * Maximum number of combining characters we're willing to store in a
+ * character cell. Our linked-list data representation permits an
+ * unlimited number of these in principle, but if we allowed that in
+ * practice then it would be an easy DoS to just squirt a squillion
+ * identical combining characters to someone's terminal and cause
+ * their PuTTY or pterm to consume lots of memory and CPU pointlessly.
+ *
+ * The precise figure of 32 is more or less arbitrary, but one point
+ * supporting it is UAX #15's comment that 30 combining characters is
+ * "significantly beyond what is required for any linguistic or
+ * technical usage".
+ */
+#define CC_LIMIT 32
+
+/* ----------------------------------------------------------------------
+ * Helper functions for dealing with the small 'pos' structure.
+ */
+
+static inline bool poslt(pos p1, pos p2)
+{
+    if (p1.y != p2.y)
+        return p1.y < p2.y;
+    return p1.x < p2.x;
+}
+
+static inline bool posle(pos p1, pos p2)
+{
+    if (p1.y != p2.y)
+        return p1.y < p2.y;
+    return p1.x <= p2.x;
+}
+
+static inline bool poseq(pos p1, pos p2)
+{
+    return p1.y == p2.y && p1.x == p2.x;
+}
+
+static inline int posdiff_fn(pos p1, pos p2, int cols)
+{
+    return (p1.y - p2.y) * (cols+1) + (p1.x - p2.x);
+}
+
+/* Convenience wrapper on posdiff_fn which uses the 'Terminal *term'
+ * that more or less every function in terminal.c will have in scope.
+ * For safety's sake I include a TYPECHECK that ensures it really is a
+ * structure pointer of the right type. */
+#define GET_TERM_COLS TYPECHECK(term == (Terminal *)0, term->cols)
+#define posdiff(p1,p2) posdiff_fn(p1, p2, GET_TERM_COLS)
+
+/* Product-order comparisons for rectangular block selection. */
+
+static inline bool posPle(pos p1, pos p2)
+{
+    return p1.y <= p2.y && p1.x <= p2.x;
+}
+
+static inline bool posPle_left(pos p1, pos p2)
+{
+    /*
+     * This function is used for checking whether a given character
+     * cell of the terminal ought to be highlighted as part of the
+     * selection, by comparing with term->selend. term->selend stores
+     * the location one space to the right of the last highlighted
+     * character. So we want to highlight the characters that are
+     * less-or-equal (in the product order) to the character just left
+     * of p2.
+     *
+     * (Setting up term->selend that way was the easiest way to get
+     * rectangular selection working at all, in a code base that had
+     * done lexicographic selection the way I happened to have done
+     * it.)
+     */
+    return p1.y <= p2.y && p1.x < p2.x;
+}
+
+static inline bool incpos_fn(pos *p, int cols)
+{
+    if (p->x == cols) {
+        p->x = 0;
+        p->y++;
+        return true;
+    }
+    p->x++;
+    return false;
+}
+
+static inline bool decpos_fn(pos *p, int cols)
+{
+    if (p->x == 0) {
+        p->x = cols;
+        p->y--;
+        return true;
+    }
+    p->x--;
+    return false;
+}
+
+/* Convenience wrappers on incpos and decpos which use term->cols
+ * (similarly to posdiff above), and also (for mild convenience and
+ * mostly historical inertia) let you leave off the & at every call
+ * site. */
+#define incpos(p) incpos_fn(&(p), GET_TERM_COLS)
+#define decpos(p) decpos_fn(&(p), GET_TERM_COLS)
+
+#endif
diff --git putty-0.81/version.h putty-url-0.81/version.h
index 2f414d7..f14c4b8 100644
--- putty-0.81/version.h
+++ putty-url-0.81/version.h
@@ -1,5 +1,5 @@
 /* Generated by automated build script */
 #define RELEASE 0.81
-#define TEXTVER "Release 0.81"
+#define TEXTVER "Release 0.81 (url)"
 #define SSHVER "-Release-0.81"
 #define BINARY_VERSION 0,81,0,0
diff --git putty-url-0.81/windows/.config.c.swp putty-url-0.81/windows/.config.c.swp
new file mode 100644
index 0000000..e69de29
diff --git putty-0.81/windows/CMakeLists.txt putty-url-0.81/windows/CMakeLists.txt
index d34b410..2baf30e 100644
--- putty-0.81/windows/CMakeLists.txt
+++ putty-url-0.81/windows/CMakeLists.txt
@@ -184,3 +184,22 @@ target_link_libraries(test_split_into_argv utils ${platform_libraries})
 add_executable(test_screenshot
   test_screenshot.c)
 target_link_libraries(test_screenshot utils ${platform_libraries})
+
+
+# urlhack
+add_library(re_lib STATIC)
+add_subdirectory(re_lib)
+
+add_library(urlhack STATIC
+  urlhack.c)
+add_dependencies(urlhack re_lib)
+target_link_libraries(urlhack re_lib)
+
+add_dependencies(putty urlhack)
+add_dependencies(puttytel urlhack)
+add_dependencies(pterm urlhack)
+
+target_link_libraries(putty urlhack)
+target_link_libraries(puttytel urlhack)
+target_link_libraries(pterm urlhack)
+# end urlhack
diff --git putty-0.81/windows/config.c putty-url-0.81/windows/config.c
index fc9070b..3bfacaf 100644
--- putty-0.81/windows/config.c
+++ putty-url-0.81/windows/config.c
@@ -381,4 +381,49 @@ void win_setup_config_box(struct controlbox *b, HWND *hwndp, bool has_help,
                      HELPCTX(ssh_tunnels_xauthority),
                      conf_filesel_handler, I(CONF_xauthfile));
     }
+
+    /*
+     * HACK: PuttyTray / Nutty
+     * Hyperlink stuff: The Window/Hyperlinks panel.
+     */
+    ctrl_settitle(b, "Window/Hyperlinks", "Options controlling behaviour of hyperlinks");
+    s = ctrl_getset(b, "Window/Hyperlinks", "general", "General options for hyperlinks");
+    
+    ctrl_radiobuttons(s, "Underline hyperlinks:", 'u', 1,
+                      HELPCTX(no_help),
+                      conf_radiobutton_handler,
+                      I(CONF_url_underline),
+                      "Always", I(URLHACK_UNDERLINE_ALWAYS),
+                      "When hovered upon", I(URLHACK_UNDERLINE_HOVER),
+                      "Never", I(URLHACK_UNDERLINE_NEVER));
+    
+    ctrl_checkbox(s, "Use ctrl+click to launch hyperlinks", 'l',
+                     HELPCTX(no_help),
+                     conf_checkbox_handler, I(CONF_url_ctrl_click));
+    
+    s = ctrl_getset(b, "Window/Hyperlinks", "browser", "Browser application");
+    
+    ctrl_checkbox(s, "Use the default browser", 'b',
+                     HELPCTX(no_help),
+                     conf_checkbox_handler, I(CONF_url_defbrowser));
+    
+    ctrl_filesel(s, "or specify an application to open hyperlinks with:", 's',
+                 "Application (*.exe)\0*.exe\0All files (*.*)\0*.*\0\0", TRUE,
+                 "Select executable to open hyperlinks with", HELPCTX(no_help),
+                 conf_filesel_handler, I(CONF_url_browser));
+    
+    s = ctrl_getset(b, "Window/Hyperlinks", "regexp", "Regular expression");
+    
+    ctrl_checkbox(s, "Use the default regular expression", 'r',
+                  HELPCTX(no_help),
+                  conf_checkbox_handler, I(CONF_url_defregex));
+    
+    ctrl_editbox(s, "or specify your own:", NO_SHORTCUT, 100,
+                 HELPCTX(no_help),
+                 conf_editbox_handler,
+                 I(CONF_url_regex),
+                 ED_STR);
+    
+    ctrl_text(s, "The single white space will be cropped in front of the link, if exists.",
+              HELPCTX(no_help));
 }
diff --git putty-0.81/windows/dialog.c putty-url-0.81/windows/dialog.c
index 03d6380..adff142 100644
--- putty-0.81/windows/dialog.c
+++ putty-url-0.81/windows/dialog.c
@@ -412,7 +412,7 @@ static INT_PTR CALLBACK AboutProc(HWND hwnd, UINT msg,
           case IDA_WEB:
             /* Load web browser */
             ShellExecute(hwnd, "open",
-                         "https://www.chiark.greenend.org.uk/~sgtatham/putty/",
+                         "https://ryara.net/putty-url/",
                          0, 0, SW_SHOWDEFAULT);
             return 0;
         }
diff --git putty-url-0.81/windows/re_lib/CMakeLists.txt putty-url-0.81/windows/re_lib/CMakeLists.txt
new file mode 100644
index 0000000..f871991
--- /dev/null
+++ putty-url-0.81/windows/re_lib/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_sources_from_current_dir(re_lib
+  regexp.c)
diff --git putty-url-0.81/windows/re_lib/regexp.c putty-url-0.81/windows/re_lib/regexp.c
new file mode 100644
index 0000000..146578e
--- /dev/null
+++ putty-url-0.81/windows/re_lib/regexp.c
@@ -0,0 +1,1212 @@
+/*
+ * regcomp and regexec -- regsub and regerror are elsewhere
+ * @(#)regexp.c	1.3 of 18 April 87
+ *
+ *	Copyright (c) 1986 by University of Toronto.
+ *	Written by Henry Spencer.  Not derived from licensed software.
+ *
+ *	Permission is granted to anyone to use this software for any
+ *	purpose on any computer system, and to redistribute it freely,
+ *	subject to the following restrictions:
+ *
+ *	1. The author is not responsible for the consequences of use of
+ *		this software, no matter how awful, even if they arise
+ *		from defects in it.
+ *
+ *	2. The origin of this software must not be misrepresented, either
+ *		by explicit claim or by omission.
+ *
+ *	3. Altered versions must be plainly marked as such, and must not
+ *		be misrepresented as being the original software.
+ *
+ * Beware that some of this code is subtly aware of the way operator
+ * precedence is structured in regular expressions.  Serious changes in
+ * regular-expression syntax might require a total rethink.
+ */
+#include <stdio.h>
+#include <malloc.h>
+#include <string.h>
+
+#include "regexp.h"
+#include "regmagic.h"
+
+/*
+ * The "internal use only" fields in regexp.h are present to pass info from
+ * compile to execute that permits the execute phase to run lots faster on
+ * simple cases.  They are:
+ *
+ * regstart	char that must begin a match; '\0' if none obvious
+ * reganch	is the match anchored (at beginning-of-line only)?
+ * regmust	string (pointer into program) that match must include, or NULL
+ * regmlen	length of regmust string
+ *
+ * Regstart and reganch permit very fast decisions on suitable starting points
+ * for a match, cutting down the work a lot.  Regmust permits fast rejection
+ * of lines that cannot possibly match.  The regmust tests are costly enough
+ * that regcomp() supplies a regmust only if the r.e. contains something
+ * potentially expensive (at present, the only such thing detected is * or +
+ * at the start of the r.e., which can involve a lot of backup).  Regmlen is
+ * supplied because the test in regexec() needs it and regcomp() is computing
+ * it anyway.
+ */
+
+/*
+ * Structure for regexp "program".  This is essentially a linear encoding
+ * of a nondeterministic finite-state machine (aka syntax charts or
+ * "railroad normal form" in parsing technology).  Each node is an opcode
+ * plus a "next" pointer, possibly plus an operand.  "Next" pointers of
+ * all nodes except BRANCH implement concatenation; a "next" pointer with
+ * a BRANCH on both ends of it is connecting two alternatives.  (Here we
+ * have one of the subtle syntax dependencies:  an individual BRANCH (as
+ * opposed to a collection of them) is never concatenated with anything
+ * because of operator precedence.)  The operand of some types of node is
+ * a literal string; for others, it is a node leading into a sub-FSM.  In
+ * particular, the operand of a BRANCH node is the first node of the branch.
+ * (NB this is *not* a tree structure:  the tail of the branch connects
+ * to the thing following the set of BRANCHes.)  The opcodes are:
+ */
+
+/* definition	number	opnd?	meaning */
+#define	END	0	/* no	End of program. */
+#define	BOL	1	/* no	Match "" at beginning of line. */
+#define	EOL	2	/* no	Match "" at end of line. */
+#define	ANY	3	/* no	Match any one character. */
+#define	ANYOF	4	/* str	Match any character in this string. */
+#define	ANYBUT	5	/* str	Match any character not in this string. */
+#define	BRANCH	6	/* node	Match this alternative, or the next... */
+#define	BACK	7	/* no	Match "", "next" ptr points backward. */
+#define	EXACTLY	8	/* str	Match this string. */
+#define	NOTHING	9	/* no	Match empty string. */
+#define	STAR	10	/* node	Match this (simple) thing 0 or more times. */
+#define	PLUS	11	/* node	Match this (simple) thing 1 or more times. */
+#define	OPEN	20	/* no	Mark this point in input as start of #n. */
+			/*	OPEN+1 is number 1, etc. */
+#define	CLOSE	OPEN + NSUBEXP	/* no	Analogous to OPEN. */
+
+/*
+ * Opcode notes:
+ *
+ * BRANCH	The set of branches constituting a single choice are hooked
+ *		together with their "next" pointers, since precedence prevents
+ *		anything being concatenated to any individual branch.  The
+ *		"next" pointer of the last BRANCH in a choice points to the
+ *		thing following the whole choice.  This is also where the
+ *		final "next" pointer of each individual branch points; each
+ *		branch starts with the operand node of a BRANCH node.
+ *
+ * BACK		Normal "next" pointers all implicitly point forward; BACK
+ *		exists to make loop structures possible.
+ *
+ * STAR,PLUS	'?', and complex '*' and '+', are implemented as circular
+ *		BRANCH structures using BACK.  Simple cases (one character
+ *		per match) are implemented with STAR and PLUS for speed
+ *		and to minimize recursive plunges.
+ *
+ * OPEN,CLOSE	...are numbered at compile time.
+ */
+
+/*
+ * A node is one char of opcode followed by two chars of "next" pointer.
+ * "Next" pointers are stored as two 8-bit pieces, high order first.  The
+ * value is a positive offset from the opcode of the node containing it.
+ * An operand, if any, simply follows the node.  (Note that much of the
+ * code generation knows about this implicit relationship.)
+ *
+ * Using two bytes for the "next" pointer is vast overkill for most things,
+ * but allows patterns to get big without disasters.
+ */
+#define	OP(p)	(*(p))
+#define	NEXT(p)	(((*((p)+1)&0377)<<8) + (*((p)+2)&0377))
+#define	OPERAND(p)	((p) + 3)
+
+/*
+ * See regmagic.h for one further detail of program structure.
+ */
+
+
+/*
+ * Utility definitions.
+ */
+#ifndef CHARBITS
+#define	UCHARAT(p)	((int)*(unsigned char *)(p))
+#else
+#define	UCHARAT(p)	((int)*(p)&CHARBITS)
+#endif
+
+#define	FAIL(m)	{ regerror(m); return(NULL); }
+#define	ISMULT(c)	((c) == '*' || (c) == '+' || (c) == '?')
+#define	META	"^$.[()|?+*\\"
+
+/*
+ * Flags to be passed up and down.
+ */
+#define	HASWIDTH	01	/* Known never to match null string. */
+#define	SIMPLE		02	/* Simple enough to be STAR/PLUS operand. */
+#define	SPSTART		04	/* Starts with * or +. */
+#define	WORST		0	/* Worst case. */
+
+/*
+ * Global work variables for regcomp().
+ */
+static char *regparse;		/* Input-scan pointer. */
+static int regnpar;		/* () count. */
+static char regdummy;
+static char *regcode;		/* Code-emit pointer; &regdummy = don't. */
+static long regsize;		/* Code size. */
+
+/*
+ * Forward declarations for regcomp()'s friends.
+ */
+#ifndef STATIC
+#define	STATIC	static
+#endif
+STATIC char* reg( int paren, int* flagp);
+STATIC char* regbranch( int* flagp);
+STATIC char* regpiece( int* flagp);
+STATIC char* regatom( int* flagp);
+STATIC char* regnode( char op);
+STATIC char* regnext( char* p);
+STATIC void regc( char c);
+STATIC void reginsert( char op, char* opnd);
+STATIC void regtail( char* p, char* val);
+STATIC void regoptail( char* p,  char* val);
+#ifdef STRCSPN
+STATIC int strcspn();
+#endif
+
+static void (*regerror_func)( char* s) = 0;
+
+void regerror( char* s)
+{
+	if( regerror_func)
+		(*regerror_func)( s);
+}
+
+void set_regerror_func( void (*func)( char*))
+{
+	regerror_func = func;
+}
+
+/*
+ - regcomp - compile a regular expression into internal code
+ *
+ * We can't allocate space until we know how big the compiled form will be,
+ * but we can't compile it (and thus know how big it is) until we've got a
+ * place to put the code.  So we cheat:  we compile it twice, once with code
+ * generation turned off and size counting turned on, and once "for real".
+ * This also means that we don't allocate space until we are sure that the
+ * thing really will compile successfully, and we never have to move the
+ * code and thus invalidate pointers into it.  (Note that it has to be in
+ * one piece because free() must be able to free it all.)
+ *
+ * Beware that the optimization-preparation code in here knows about some
+ * of the structure of the compiled regexp.
+ */
+regexp *
+regcomp( char* exp)
+{
+	register regexp *r;
+	register char *scan;
+	register char *longest;
+	register int len;
+	int flags;
+
+	if (exp == NULL)
+		FAIL("NULL argument");
+
+	/* First pass: determine size, legality. */
+	regparse = exp;
+	regnpar = 1;
+	regsize = 0L;
+	regcode = &regdummy;
+	regc(MAGIC);
+	if (reg(0, &flags) == NULL)
+		return(NULL);
+
+	/* Small enough for pointer-storage convention? */
+	if (regsize >= 32767L)		/* Probably could be 65535L. */
+		FAIL("regexp too big");
+
+	/* Allocate space. */
+	r = (regexp *)malloc(sizeof(regexp) + (unsigned)regsize);
+	if (r == NULL)
+		FAIL("out of space");
+
+	/* Second pass: emit code. */
+	regparse = exp;
+	regnpar = 1;
+	regcode = r->program;
+	regc(MAGIC);
+	if (reg(0, &flags) == NULL)
+		return(NULL);
+
+	/* Dig out information for optimizations. */
+	r->regstart = '\0';	/* Worst-case defaults. */
+	r->reganch = 0;
+	r->regmust = NULL;
+	r->regmlen = 0;
+	scan = r->program+1;			/* First BRANCH. */
+	if (OP(regnext(scan)) == END) {		/* Only one top-level choice. */
+		scan = OPERAND(scan);
+
+		/* Starting-point info. */
+		if (OP(scan) == EXACTLY)
+			r->regstart = *OPERAND(scan);
+		else if (OP(scan) == BOL)
+			r->reganch++;
+
+		/*
+		 * If there's something expensive in the r.e., find the
+		 * longest literal string that must appear and make it the
+		 * regmust.  Resolve ties in favor of later strings, since
+		 * the regstart check works with the beginning of the r.e.
+		 * and avoiding duplication strengthens checking.  Not a
+		 * strong reason, but sufficient in the absence of others.
+		 */
+		if (flags&SPSTART) {
+			longest = NULL;
+			len = 0;
+			for (; scan != NULL; scan = regnext(scan))
+				if (OP(scan) == EXACTLY && strlen(OPERAND(scan)) >= len) {
+					longest = OPERAND(scan);
+					len = strlen(OPERAND(scan));
+				}
+			r->regmust = longest;
+			r->regmlen = len;
+		}
+	}
+
+	return(r);
+}
+
+/*
+ - reg - regular expression, i.e. main body or parenthesized thing
+ *
+ * Caller must absorb opening parenthesis.
+ *
+ * Combining parenthesis handling with the base level of regular expression
+ * is a trifle forced, but the need to tie the tails of the branches to what
+ * follows makes it hard to avoid.
+ */
+static char *
+reg( int paren, int* flagp)	/* paren - Parenthesized? */
+{
+	register char *ret;
+	register char *br;
+	register char *ender;
+	register int parno;
+	int flags;
+
+	*flagp = HASWIDTH;	/* Tentatively. */
+
+	/* Make an OPEN node, if parenthesized. */
+	if (paren) {
+		if (regnpar >= NSUBEXP)
+			FAIL("too many ()");
+		parno = regnpar;
+		regnpar++;
+		ret = regnode(OPEN+parno);
+	} else
+		ret = NULL;
+
+	/* Pick up the branches, linking them together. */
+	br = regbranch(&flags);
+	if (br == NULL)
+		return(NULL);
+	if (ret != NULL)
+		regtail(ret, br);	/* OPEN -> first. */
+	else
+		ret = br;
+	if (!(flags&HASWIDTH))
+		*flagp &= ~HASWIDTH;
+	*flagp |= flags&SPSTART;
+	while (*regparse == '|') {
+		regparse++;
+		br = regbranch(&flags);
+		if (br == NULL)
+			return(NULL);
+		regtail(ret, br);	/* BRANCH -> BRANCH. */
+		if (!(flags&HASWIDTH))
+			*flagp &= ~HASWIDTH;
+		*flagp |= flags&SPSTART;
+	}
+
+	/* Make a closing node, and hook it on the end. */
+	ender = regnode((paren) ? CLOSE+parno : END);	
+	regtail(ret, ender);
+
+	/* Hook the tails of the branches to the closing node. */
+	for (br = ret; br != NULL; br = regnext(br))
+		regoptail(br, ender);
+
+	/* Check for proper termination. */
+	if (paren && *regparse++ != ')') {
+		FAIL("unmatched ()");
+	} else if (!paren && *regparse != '\0') {
+		if (*regparse == ')') {
+			FAIL("unmatched ()");
+		} else
+			FAIL("junk on end");	/* "Can't happen". */
+		/* NOTREACHED */
+	}
+
+	return(ret);
+}
+
+/*
+ - regbranch - one alternative of an | operator
+ *
+ * Implements the concatenation operator.
+ */
+static char *
+regbranch( int* flagp)
+{
+	register char *ret;
+	register char *chain;
+	register char *latest;
+	int flags;
+
+	*flagp = WORST;		/* Tentatively. */
+
+	ret = regnode(BRANCH);
+	chain = NULL;
+	while (*regparse != '\0' && *regparse != '|' && *regparse != ')') {
+		latest = regpiece(&flags);
+		if (latest == NULL)
+			return(NULL);
+		*flagp |= flags&HASWIDTH;
+		if (chain == NULL)	/* First piece. */
+			*flagp |= flags&SPSTART;
+		else
+			regtail(chain, latest);
+		chain = latest;
+	}
+	if (chain == NULL)	/* Loop ran zero times. */
+		(void) regnode(NOTHING);
+
+	return(ret);
+}
+
+/*
+ - regpiece - something followed by possible [*+?]
+ *
+ * Note that the branching code sequences used for ? and the general cases
+ * of * and + are somewhat optimized:  they use the same NOTHING node as
+ * both the endmarker for their branch list and the body of the last branch.
+ * It might seem that this node could be dispensed with entirely, but the
+ * endmarker role is not redundant.
+ */
+static char *
+regpiece( int* flagp)
+{
+	register char *ret;
+	register char op;
+	register char *next;
+	int flags;
+
+	ret = regatom(&flags);
+	if (ret == NULL)
+		return(NULL);
+
+	op = *regparse;
+	if (!ISMULT(op)) {
+		*flagp = flags;
+		return(ret);
+	}
+
+	if (!(flags&HASWIDTH) && op != '?')
+		FAIL("*+ operand could be empty");
+	*flagp = (op != '+') ? (WORST|SPSTART) : (WORST|HASWIDTH);
+
+	if (op == '*' && (flags&SIMPLE))
+		reginsert(STAR, ret);
+	else if (op == '*') {
+		/* Emit x* as (x&|), where & means "self". */
+		reginsert(BRANCH, ret);			/* Either x */
+		regoptail(ret, regnode(BACK));		/* and loop */
+		regoptail(ret, ret);			/* back */
+		regtail(ret, regnode(BRANCH));		/* or */
+		regtail(ret, regnode(NOTHING));		/* null. */
+	} else if (op == '+' && (flags&SIMPLE))
+		reginsert(PLUS, ret);
+	else if (op == '+') {
+		/* Emit x+ as x(&|), where & means "self". */
+		next = regnode(BRANCH);			/* Either */
+		regtail(ret, next);
+		regtail(regnode(BACK), ret);		/* loop back */
+		regtail(next, regnode(BRANCH));		/* or */
+		regtail(ret, regnode(NOTHING));		/* null. */
+	} else if (op == '?') {
+		/* Emit x? as (x|) */
+		reginsert(BRANCH, ret);			/* Either x */
+		regtail(ret, regnode(BRANCH));		/* or */
+		next = regnode(NOTHING);		/* null. */
+		regtail(ret, next);
+		regoptail(ret, next);
+	}
+	regparse++;
+	if (ISMULT(*regparse))
+		FAIL("nested *?+");
+
+	return(ret);
+}
+
+/*
+ - regatom - the lowest level
+ *
+ * Optimization:  gobbles an entire sequence of ordinary characters so that
+ * it can turn them into a single node, which is smaller to store and
+ * faster to run.  Backslashed characters are exceptions, each becoming a
+ * separate node; the code is simpler that way and it's not worth fixing.
+ */
+static char *
+regatom( int* flagp)
+{
+	register char *ret;
+	int flags;
+
+	*flagp = WORST;		/* Tentatively. */
+
+	switch (*regparse++) {
+	case '^':
+		ret = regnode(BOL);
+		break;
+	case '$':
+		ret = regnode(EOL);
+		break;
+	case '.':
+		ret = regnode(ANY);
+		*flagp |= HASWIDTH|SIMPLE;
+		break;
+	case '[': {
+			int rclass;
+			int classend;
+
+			if (*regparse == '^') {	/* Complement of range. */
+				ret = regnode(ANYBUT);
+				regparse++;
+			} else
+				ret = regnode(ANYOF);
+			if (*regparse == ']' || *regparse == '-')
+				regc(*regparse++);
+			while (*regparse != '\0' && *regparse != ']') {
+				if (*regparse == '-') {
+					regparse++;
+					if (*regparse == ']' || *regparse == '\0')
+						regc('-');
+					else {
+						rclass = UCHARAT(regparse-2)+1;
+						classend = UCHARAT(regparse);
+						if (rclass > classend+1)
+							FAIL("invalid [] range");
+						for (; rclass <= classend; rclass++)
+							regc(rclass);
+						regparse++;
+					}
+				} else
+					regc(*regparse++);
+			}
+			regc('\0');
+			if (*regparse != ']')
+				FAIL("unmatched []");
+			regparse++;
+			*flagp |= HASWIDTH|SIMPLE;
+		}
+		break;
+	case '(':
+		ret = reg(1, &flags);
+		if (ret == NULL)
+			return(NULL);
+		*flagp |= flags&(HASWIDTH|SPSTART);
+		break;
+	case '\0':
+	case '|':
+	case ')':
+		FAIL("internal urp");	/* Supposed to be caught earlier. */
+		break;
+	case '?':
+	case '+':
+	case '*':
+		FAIL("?+* follows nothing");
+		break;
+	case '\\':
+		if (*regparse == '\0')
+			FAIL("trailing \\");
+		ret = regnode(EXACTLY);
+		regc(*regparse++);
+		regc('\0');
+		*flagp |= HASWIDTH|SIMPLE;
+		break;
+	default: {
+			register int len;
+			register char ender;
+
+			regparse--;
+			len = strcspn(regparse, META);
+			if (len <= 0)
+				FAIL("internal disaster");
+			ender = *(regparse+len);
+			if (len > 1 && ISMULT(ender))
+				len--;		/* Back off clear of ?+* operand. */
+			*flagp |= HASWIDTH;
+			if (len == 1)
+				*flagp |= SIMPLE;
+			ret = regnode(EXACTLY);
+			while (len > 0) {
+				regc(*regparse++);
+				len--;
+			}
+			regc('\0');
+		}
+		break;
+	}
+
+	return(ret);
+}
+
+/*
+ - regnode - emit a node
+ */
+static char *			/* Location. */
+regnode( char op)
+{
+	register char *ret;
+	register char *ptr;
+
+	ret = regcode;
+	if (ret == &regdummy) {
+		regsize += 3;
+		return(ret);
+	}
+
+	ptr = ret;
+	*ptr++ = op;
+	*ptr++ = '\0';		/* Null "next" pointer. */
+	*ptr++ = '\0';
+	regcode = ptr;
+
+	return(ret);
+}
+
+/*
+ - regc - emit (if appropriate) a byte of code
+ */
+static void
+regc( char b)
+{
+	if (regcode != &regdummy)
+		*regcode++ = b;
+	else
+		regsize++;
+}
+
+/*
+ - reginsert - insert an operator in front of already-emitted operand
+ *
+ * Means relocating the operand.
+ */
+static void
+reginsert( char op, char* opnd)
+{
+	register char *src;
+	register char *dst;
+	register char *place;
+
+	if (regcode == &regdummy) {
+		regsize += 3;
+		return;
+	}
+
+	src = regcode;
+	regcode += 3;
+	dst = regcode;
+	while (src > opnd)
+		*--dst = *--src;
+
+	place = opnd;		/* Op node, where operand used to be. */
+	*place++ = op;
+	*place++ = '\0';
+	*place++ = '\0';
+}
+
+/*
+ - regtail - set the next-pointer at the end of a node chain
+ */
+static void
+regtail( char* p, char* val)
+{
+	register char *scan;
+	register char *temp;
+	register int offset;
+
+	if (p == &regdummy)
+		return;
+
+	/* Find last node. */
+	scan = p;
+	for (;;) {
+		temp = regnext(scan);
+		if (temp == NULL)
+			break;
+		scan = temp;
+	}
+
+	if (OP(scan) == BACK)
+		offset = scan - val;
+	else
+		offset = val - scan;
+	*(scan+1) = (offset>>8)&0377;
+	*(scan+2) = offset&0377;
+}
+
+/*
+ - regoptail - regtail on operand of first argument; nop if operandless
+ */
+static void
+regoptail( char* p,  char* val)
+{
+	/* "Operandless" and "op != BRANCH" are synonymous in practice. */
+	if (p == NULL || p == &regdummy || OP(p) != BRANCH)
+		return;
+	regtail(OPERAND(p), val);
+}
+
+/*
+ * regexec and friends
+ */
+
+/*
+ * Global work variables for regexec().
+ */
+static char *reginput;		/* String-input pointer. */
+static char *regbol;		/* Beginning of input, for ^ check. */
+static char **regstartp;	/* Pointer to startp array. */
+static char **regendp;		/* Ditto for endp. */
+
+/*
+ * Forwards.
+ */
+STATIC int regtry( regexp* prog, char* string);
+STATIC int regmatch( char* prog);
+STATIC int regrepeat( char* p);
+
+#ifdef DEBUG
+int regnarrate = 0;
+void regdump( regexp* r);
+STATIC char* regprop( char* op);
+#endif
+
+/*
+ - regexec - match a regexp against a string
+ */
+int
+regexec( regexp* prog, char* string)
+{
+	register char *s;
+
+	/* Be paranoid... */
+	if (prog == NULL || string == NULL) {
+		regerror("NULL parameter");
+		return(0);
+	}
+
+	/* Check validity of program. */
+	if (UCHARAT(prog->program) != MAGIC) {
+		regerror("corrupted program");
+		return(0);
+	}
+
+	/* If there is a "must appear" string, look for it. */
+	if (prog->regmust != NULL) {
+		s = string;
+		while ((s = strchr(s, prog->regmust[0])) != NULL) {
+			if (strncmp(s, prog->regmust, prog->regmlen) == 0)
+				break;	/* Found it. */
+			s++;
+		}
+		if (s == NULL)	/* Not present. */
+			return(0);
+	}
+
+	/* Mark beginning of line for ^ . */
+	regbol = string;
+
+	/* Simplest case:  anchored match need be tried only once. */
+	if (prog->reganch)
+		return(regtry(prog, string));
+
+	/* Messy cases:  unanchored match. */
+	s = string;
+	if (prog->regstart != '\0')
+		/* We know what char it must start with. */
+		while ((s = strchr(s, prog->regstart)) != NULL) {
+			if (regtry(prog, s))
+				return(1);
+			s++;
+		}
+	else
+		/* We don't -- general case. */
+		do {
+			if (regtry(prog, s))
+				return(1);
+		} while (*s++ != '\0');
+
+	/* Failure. */
+	return(0);
+}
+
+/*
+ - regtry - try match at specific point
+ */
+static int			/* 0 failure, 1 success */
+regtry( regexp* prog, char* string)
+{
+	register int i;
+	register char **sp;
+	register char **ep;
+
+	reginput = string;
+	regstartp = prog->startp;
+	regendp = prog->endp;
+
+	sp = prog->startp;
+	ep = prog->endp;
+	for (i = NSUBEXP; i > 0; i--) {
+		*sp++ = NULL;
+		*ep++ = NULL;
+	}
+	if (regmatch(prog->program + 1)) {
+		prog->startp[0] = string;
+		prog->endp[0] = reginput;
+		return(1);
+	} else
+		return(0);
+}
+
+/*
+ - regmatch - main matching routine
+ *
+ * Conceptually the strategy is simple:  check to see whether the current
+ * node matches, call self recursively to see whether the rest matches,
+ * and then act accordingly.  In practice we make some effort to avoid
+ * recursion, in particular by going through "ordinary" nodes (that don't
+ * need to know whether the rest of the match failed) by a loop instead of
+ * by recursion.
+ */
+static int			/* 0 failure, 1 success */
+regmatch( char* prog)
+{
+	register char *scan;	/* Current node. */
+	char *next;		/* Next node. */
+
+	scan = prog;
+#ifdef DEBUG
+	if (scan != NULL && regnarrate)
+		fprintf(stderr, "%s(\n", regprop(scan));
+#endif
+	while (scan != NULL) {
+#ifdef DEBUG
+		if (regnarrate)
+			fprintf(stderr, "%s...\n", regprop(scan));
+#endif
+		next = regnext(scan);
+
+		switch (OP(scan)) {
+		case BOL:
+			if (reginput != regbol)
+				return(0);
+			break;
+		case EOL:
+			if (*reginput != '\0')
+				return(0);
+			break;
+		case ANY:
+			if (*reginput == '\0')
+				return(0);
+			reginput++;
+			break;
+		case EXACTLY: {
+				register int len;
+				register char *opnd;
+
+				opnd = OPERAND(scan);
+				/* Inline the first character, for speed. */
+				if (*opnd != *reginput)
+					return(0);
+				len = strlen(opnd);
+				if (len > 1 && strncmp(opnd, reginput, len) != 0)
+					return(0);
+				reginput += len;
+			}
+			break;
+		case ANYOF:
+			if (*reginput == '\0' || strchr(OPERAND(scan), *reginput) == NULL)
+				return(0);
+			reginput++;
+			break;
+		case ANYBUT:
+			if (*reginput == '\0' || strchr(OPERAND(scan), *reginput) != NULL)
+				return(0);
+			reginput++;
+			break;
+		case NOTHING:
+			break;
+		case BACK:
+			break;
+		case OPEN+1:
+		case OPEN+2:
+		case OPEN+3:
+		case OPEN+4:
+		case OPEN+5:
+		case OPEN+6:
+		case OPEN+7:
+		case OPEN+8:
+		case OPEN+9:
+		case OPEN+10:
+		case OPEN+11:
+		case OPEN+12:
+		case OPEN+13:
+		case OPEN+14:
+		case OPEN+15: {
+				register int no;
+				register char *save;
+
+				no = OP(scan) - OPEN;
+				save = reginput;
+
+				if (regmatch(next)) {
+					/*
+					 * Don't set startp if some later
+					 * invocation of the same parentheses
+					 * already has.
+					 */
+					if (regstartp[no] == NULL)
+						regstartp[no] = save;
+					return(1);
+				} else
+					return(0);
+			}
+			break;
+		case CLOSE+1:
+		case CLOSE+2:
+		case CLOSE+3:
+		case CLOSE+4:
+		case CLOSE+5:
+		case CLOSE+6:
+		case CLOSE+7:
+		case CLOSE+8:
+		case CLOSE+9:
+		case CLOSE+10:
+		case CLOSE+11:
+		case CLOSE+12:
+		case CLOSE+13:
+		case CLOSE+14:
+		case CLOSE+15: {
+				register int no;
+				register char *save;
+
+				no = OP(scan) - CLOSE;
+				save = reginput;
+
+				if (regmatch(next)) {
+					/*
+					 * Don't set endp if some later
+					 * invocation of the same parentheses
+					 * already has.
+					 */
+					if (regendp[no] == NULL)
+						regendp[no] = save;
+					return(1);
+				} else
+					return(0);
+			}
+			break;
+		case BRANCH: {
+				register char *save;
+
+				if (OP(next) != BRANCH)		/* No choice. */
+					next = OPERAND(scan);	/* Avoid recursion. */
+				else {
+					do {
+						save = reginput;
+						if (regmatch(OPERAND(scan)))
+							return(1);
+						reginput = save;
+						scan = regnext(scan);
+					} while (scan != NULL && OP(scan) == BRANCH);
+					return(0);
+					/* NOTREACHED */
+				}
+			}
+			break;
+		case STAR:
+		case PLUS: {
+				register char nextch;
+				register int no;
+				register char *save;
+				register int min;
+
+				/*
+				 * Lookahead to avoid useless match attempts
+				 * when we know what character comes next.
+				 */
+				nextch = '\0';
+				if (OP(next) == EXACTLY)
+					nextch = *OPERAND(next);
+				min = (OP(scan) == STAR) ? 0 : 1;
+				save = reginput;
+				no = regrepeat(OPERAND(scan));
+				while (no >= min) {
+					/* If it could work, try it. */
+					if (nextch == '\0' || *reginput == nextch)
+						if (regmatch(next))
+							return(1);
+					/* Couldn't or didn't -- back up. */
+					no--;
+					reginput = save + no;
+				}
+				return(0);
+			}
+			break;
+		case END:
+			return(1);	/* Success! */
+			break;
+		default:
+			regerror("memory corruption");
+			return(0);
+			break;
+		}
+
+		scan = next;
+	}
+
+	/*
+	 * We get here only if there's trouble -- normally "case END" is
+	 * the terminating point.
+	 */
+	regerror("corrupted pointers");
+	return(0);
+}
+
+/*
+ - regrepeat - repeatedly match something simple, report how many
+ */
+static int
+regrepeat( char* p)
+{
+	register int count = 0;
+	register char *scan;
+	register char *opnd;
+
+	scan = reginput;
+	opnd = OPERAND(p);
+	switch (OP(p)) {
+	case ANY:
+		count = strlen(scan);
+		scan += count;
+		break;
+	case EXACTLY:
+		while (*opnd == *scan) {
+			count++;
+			scan++;
+		}
+		break;
+	case ANYOF:
+		while (*scan != '\0' && strchr(opnd, *scan) != NULL) {
+			count++;
+			scan++;
+		}
+		break;
+	case ANYBUT:
+		while (*scan != '\0' && strchr(opnd, *scan) == NULL) {
+			count++;
+			scan++;
+		}
+		break;
+	default:		/* Oh dear.  Called inappropriately. */
+		regerror("internal foulup");
+		count = 0;	/* Best compromise. */
+		break;
+	}
+	reginput = scan;
+
+	return(count);
+}
+
+/*
+ - regnext - dig the "next" pointer out of a node
+ */
+static char *
+regnext( char* p)
+{
+	register int offset;
+
+	if (p == &regdummy)
+		return(NULL);
+
+	offset = NEXT(p);
+	if (offset == 0)
+		return(NULL);
+
+	if (OP(p) == BACK)
+		return(p-offset);
+	else
+		return(p+offset);
+}
+
+#ifdef DEBUG
+
+STATIC char *regprop();
+
+/*
+ - regdump - dump a regexp onto stdout in vaguely comprehensible form
+ */
+void
+regdump( regexp* r)
+{
+	register char *s;
+	register char op = EXACTLY;	/* Arbitrary non-END op. */
+	register char *next;
+
+	s = r->program + 1;
+	while (op != END) {	/* While that wasn't END last time... */
+		op = OP(s);
+		printf("%2d%s", s-r->program, regprop(s));	/* Where, what. */
+		next = regnext(s);
+		if (next == NULL)		/* Next ptr. */
+			printf("(0)");
+		else 
+			printf("(%d)", (s-r->program)+(next-s));
+		s += 3;
+		if (op == ANYOF || op == ANYBUT || op == EXACTLY) {
+			/* Literal string, where present. */
+			while (*s != '\0') {
+				putchar(*s);
+				s++;
+			}
+			s++;
+		}
+		putchar('\n');
+	}
+
+	/* Header fields of interest. */
+	if (r->regstart != '\0')
+		printf("start `%c' ", r->regstart);
+	if (r->reganch)
+		printf("anchored ");
+	if (r->regmust != NULL)
+		printf("must have \"%s\"", r->regmust);
+	printf("\n");
+}
+
+/*
+ - regprop - printable representation of opcode
+ */
+static char *
+regprop( char* op)
+{
+	register char *p;
+	static char buf[50];
+
+	(void) strcpy(buf, ":");
+
+	switch (OP(op)) {
+	case BOL:
+		p = "BOL";
+		break;
+	case EOL:
+		p = "EOL";
+		break;
+	case ANY:
+		p = "ANY";
+		break;
+	case ANYOF:
+		p = "ANYOF";
+		break;
+	case ANYBUT:
+		p = "ANYBUT";
+		break;
+	case BRANCH:
+		p = "BRANCH";
+		break;
+	case EXACTLY:
+		p = "EXACTLY";
+		break;
+	case NOTHING:
+		p = "NOTHING";
+		break;
+	case BACK:
+		p = "BACK";
+		break;
+	case END:
+		p = "END";
+		break;
+	case OPEN+1:
+	case OPEN+2:
+	case OPEN+3:
+	case OPEN+4:
+	case OPEN+5:
+	case OPEN+6:
+	case OPEN+7:
+	case OPEN+8:
+	case OPEN+9:
+		sprintf(buf+strlen(buf), "OPEN%d", OP(op)-OPEN);
+		p = NULL;
+		break;
+	case CLOSE+1:
+	case CLOSE+2:
+	case CLOSE+3:
+	case CLOSE+4:
+	case CLOSE+5:
+	case CLOSE+6:
+	case CLOSE+7:
+	case CLOSE+8:
+	case CLOSE+9:
+		sprintf(buf+strlen(buf), "CLOSE%d", OP(op)-CLOSE);
+		p = NULL;
+		break;
+	case STAR:
+		p = "STAR";
+		break;
+	case PLUS:
+		p = "PLUS";
+		break;
+	default:
+		regerror("corrupted opcode");
+		break;
+	}
+	if (p != NULL)
+		(void) strcat(buf, p);
+	return(buf);
+}
+#endif
+
+/*
+ * The following is provided for those people who do not have strcspn() in
+ * their C libraries.  They should get off their butts and do something
+ * about it; at least one public-domain implementation of those (highly
+ * useful) string routines has been published on Usenet.
+ */
+#ifdef STRCSPN
+/*
+ * strcspn - find length of initial segment of s1 consisting entirely
+ * of characters not from s2
+ */
+
+static int
+strcspn( char* s1, char* s2)
+{
+	register char *scan1;
+	register char *scan2;
+	register int count;
+
+	count = 0;
+	for (scan1 = s1; *scan1 != '\0'; scan1++) {
+		for (scan2 = s2; *scan2 != '\0';)	/* ++ moved down. */
+			if (*scan1 == *scan2++)
+				return(count);
+		count++;
+	}
+	return(count);
+}
+#endif
diff --git putty-url-0.81/windows/re_lib/regexp.h putty-url-0.81/windows/re_lib/regexp.h
new file mode 100644
index 0000000..fe1348a
--- /dev/null
+++ putty-url-0.81/windows/re_lib/regexp.h
@@ -0,0 +1,22 @@
+/*
+ * Definitions etc. for regexp(3) routines.
+ *
+ * Caveat:  this is V8 regexp(3) [actually, a reimplementation thereof],
+ * not the System V one.
+ */
+#define NSUBEXP  16
+typedef struct regexp {
+	char *startp[NSUBEXP];
+	char *endp[NSUBEXP];
+	char regstart;		/* Internal use only. */
+	char reganch;		/* Internal use only. */
+	char *regmust;		/* Internal use only. */
+	int regmlen;		/* Internal use only. */
+	char program[1];	/* Unwarranted chumminess with compiler. */
+} regexp;
+
+regexp *regcomp( char* re);
+int regexec( regexp* r, char* str);
+void regsub( regexp* r, char* str, char* substr);
+void regerror( char* s);	/* for internal use only */
+void set_regerror_func( void (*func)( char*));
diff --git putty-url-0.81/windows/re_lib/regmagic.h putty-url-0.81/windows/re_lib/regmagic.h
new file mode 100644
index 0000000..5acf447
--- /dev/null
+++ putty-url-0.81/windows/re_lib/regmagic.h
@@ -0,0 +1,5 @@
+/*
+ * The first byte of the regexp internal "program" is actually this magic
+ * number; the start node begins in the second byte.
+ */
+#define	MAGIC	0234
diff --git putty-url-0.81/windows/urlhack.c putty-url-0.81/windows/urlhack.c
new file mode 100644
index 0000000..69d4854
--- /dev/null
+++ putty-url-0.81/windows/urlhack.c
@@ -0,0 +1,229 @@
+/*
+ * HACK: PuttyTray / Nutty
+ */
+#include <windows.h>
+#include <string.h>
+#include "urlhack.h"
+#include "misc.h"
+#include "puttymem.h"
+
+int urlhack_mouse_old_x = -1, urlhack_mouse_old_y = -1, urlhack_current_region = -1;
+
+static text_region **link_regions;
+static unsigned int link_regions_len;
+static unsigned int link_regions_current_pos;
+
+int urlhack_is_in_link_region(int x, int y)
+{
+    unsigned int i = 0;
+
+    while (i != link_regions_current_pos) {
+        text_region r = *link_regions[i];
+
+        if ((r.y0 == r.y1 && y == r.y0 && y == r.y1 && x >= r.x0 && x < r.x1) ||
+            (r.y0 != r.y1 && ((y == r.y0 && x >= r.x0) || (y == r.y1 && x < r.x1) || (y > r.y0 && y < r.y1))))
+            return i+1;
+        i++;
+    }
+
+    return 0;
+}
+
+int urlhack_is_in_this_link_region(text_region r, int x, int y)
+{
+    if ((r.y0 == r.y1 && y == r.y0 && y == r.y1 && x >= r.x0 && x < r.x1) ||
+        (r.y0 != r.y1 && ((y == r.y0 && x >= r.x0) || (y == r.y1 && x < r.x1) || (y > r.y0 && y < r.y1)))) {
+        return 1;
+    }
+
+    return 0;
+}
+
+text_region urlhack_get_link_bounds(int x, int y)
+{
+    unsigned int i = 0;
+    text_region region;
+
+    while (i != link_regions_current_pos) {
+        text_region r = *link_regions[i];
+
+        if ((r.y0 == r.y1 && y == r.y0 && y == r.y1 && x >= r.x0 && x < r.x1) ||
+            (r.y0 != r.y1 && ((y == r.y0 && x >= r.x0) || (y == r.y1 && x < r.x1) || (y > r.y0 && y < r.y1)))) {
+            return *link_regions[i];
+        }
+
+        i++;
+    }
+
+    region.x0 = region.y0 = region.x1 = region.y1 = -1;
+    return region;
+}
+
+text_region urlhack_get_link_region(int index)
+{
+    text_region region;
+
+    if (index < 0 || index >= link_regions_current_pos) {
+        region.x0 = region.y0 = region.x1 = region.y1 = -1;
+        return region;
+    }
+    else {
+        return *link_regions[index];
+    }
+}
+
+void urlhack_add_link_region(int x0, int y0, int x1, int y1)
+{
+    if (link_regions_current_pos >= link_regions_len) {
+        unsigned int i;
+        link_regions_len *= 2;
+        link_regions = sresize(link_regions, link_regions_len, text_region*);
+        for (i = link_regions_current_pos; i < link_regions_len; ++i) {
+            link_regions[i] = NULL;
+        }
+    }
+
+    link_regions[link_regions_current_pos] = snew(text_region);
+    link_regions[link_regions_current_pos]->x0 = x0;
+    link_regions[link_regions_current_pos]->y0 = y0;
+    link_regions[link_regions_current_pos]->x1 = x1;
+    link_regions[link_regions_current_pos]->y1 = y1;
+
+    link_regions_current_pos++;
+}
+
+void urlhack_launch_url(const char* app, const char *url)
+{
+    if (app) {
+        ShellExecute(NULL, NULL, app, url, NULL, SW_SHOWNORMAL);
+    } else {
+        ShellExecute(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL);
+    }
+}
+
+int urlhack_is_ctrl_pressed()
+{
+    return HIWORD(GetAsyncKeyState(VK_CONTROL));
+}
+
+void urlhack_link_regions_clear()
+{
+    unsigned int i;
+    for (i = 0; i < link_regions_len; ++i) {
+        if (link_regions[i] != NULL) {
+            sfree(link_regions[i]);
+            link_regions[i] = NULL;
+        }
+    }
+    link_regions_current_pos = 0;
+}
+
+// Regular expression stuff
+
+static int urlhack_disabled = 0;
+static int is_regexp_compiled = 0;
+static regexp* urlhack_rx;
+
+static char *window_text;
+static int window_text_len;
+static int window_text_current_pos;
+
+void urlhack_init()
+{
+    unsigned int i;
+
+    /* 32 links seems like a sane base value */
+    link_regions_current_pos = 0;
+    link_regions_len = 32;
+    link_regions = snewn(link_regions_len, text_region*);
+
+    for (i = 0; i < link_regions_len; ++i) {
+        link_regions[i] = NULL;
+    }
+
+    /* Start with default terminal size */
+    window_text_len = 80*24+1;
+    window_text = snewn(window_text_len, char);
+    urlhack_reset();
+}
+
+void urlhack_cleanup()
+{
+    urlhack_link_regions_clear();
+    sfree(link_regions);
+    sfree(window_text);
+}
+
+void urlhack_putchar(char ch)
+{
+    if (window_text_current_pos >= window_text_len) {
+        window_text = sresize(window_text, 2 * window_text_len, char);
+        memset(window_text + window_text_current_pos, '\0', window_text_len - window_text_current_pos);
+        window_text_len *= 2;
+    }
+    window_text[window_text_current_pos++] = ch;
+}
+
+void urlhack_reset()
+{
+    memset(window_text, '\0', window_text_len);
+    window_text_current_pos = 0;
+}
+
+static void rtfm(char *error)
+{
+    char std_msg[] = "The following error occured when compiling the regular expression\n" \
+        "for the hyperlink support. Hyperlink detection is disabled during\n" \
+        "this session (restart PuTTY Tray to try again).\n\n";
+
+    char *full_msg = dupprintf("%s%s", std_msg, error);
+
+    MessageBox(0, full_msg, "PuTTY Tray Error", MB_OK);
+    free(full_msg);
+}
+
+void urlhack_set_regular_expression(const char* expression)
+{
+    is_regexp_compiled = 0;
+    urlhack_disabled = 0;
+
+    set_regerror_func(rtfm);
+    urlhack_rx = regcomp((char*)(expression));
+
+    if (urlhack_rx == 0) {
+        urlhack_disabled = 1;
+    }
+
+    is_regexp_compiled = 1;
+}
+
+void urlhack_go_find_me_some_hyperlinks(int screen_width)
+{
+    char* text_pos;
+
+    if (urlhack_disabled != 0) return;
+
+    if (is_regexp_compiled == 0) {
+        urlhack_set_regular_expression(URLHACK_DEFAULT_REGEX);
+    }
+
+    urlhack_link_regions_clear();
+
+    text_pos = window_text;
+
+    while (regexec(urlhack_rx, text_pos) == 1) {
+        char* start_pos = *urlhack_rx->startp[0] == ' ' ? urlhack_rx->startp[0] + 1: urlhack_rx->startp[0];
+
+        int x0 = (start_pos - window_text) % screen_width;
+        int y0 = (start_pos - window_text) / screen_width;
+        int x1 = (urlhack_rx->endp[0] - window_text) % screen_width;
+        int y1 = (urlhack_rx->endp[0] - window_text) / screen_width;
+
+        if (x0 >= screen_width) x0 = screen_width - 1;
+        if (x1 >= screen_width) x1 = screen_width - 1;
+
+        urlhack_add_link_region(x0, y0, x1, y1);
+
+        text_pos = urlhack_rx->endp[0] + 1;
+    }
+}
diff --git putty-url-0.81/windows/urlhack.h putty-url-0.81/windows/urlhack.h
new file mode 100644
index 0000000..621236d
--- /dev/null
+++ putty-url-0.81/windows/urlhack.h
@@ -0,0 +1,36 @@
+/*
+ * HACK: PuttyTray / Nutty
+ * Hyperlink stuff: CORE FILE! Don't forget to COPY IT TO THE NEXT VERSION
+ */
+#ifndef _URLHACK_H
+#define _URLHACK_H
+
+#include "re_lib/regexp.h"
+
+typedef struct { int x0, y0, x1, y1; } text_region;
+
+/*
+ * ((https?|ftp)://|www\.).(([^ ]*\([^ ]*\))([^ ()]*[^ ,;.:"')>])?|([^ ()]*[^ ,;.:"')>]))
+ */
+#define URLHACK_DEFAULT_REGEX "((https?|ftp)://|www\\.).(([^ ]*\\([^ ]*\\))([^ ()]*[^ ,;.:\"')>])?|([^ ()]*[^ ,;.:\"')>]))"
+
+extern int urlhack_mouse_old_x, urlhack_mouse_old_y, urlhack_current_region;
+
+void urlhack_reset();
+void urlhack_go_find_me_some_hyperlinks(int screen_width);
+void urlhack_putchar(char ch);
+text_region urlhack_get_link_region(int index);
+
+int urlhack_is_in_link_region(int x, int y);
+int urlhack_is_in_this_link_region(text_region r, int x, int y);
+text_region urlhack_get_link_bounds(int x, int y);
+void urlhack_add_link_region(int x0, int y0, int x1, int y1);
+void urlhack_launch_url(const char* app, const char *url);
+int urlhack_is_ctrl_pressed();
+void urlhack_set_regular_expression(const char* expression);
+
+void urlhack_init();
+void urlhack_cleanup();
+
+
+#endif // _URLHACK_H
diff --git putty-0.81/windows/utils/defaults.c putty-url-0.81/windows/utils/defaults.c
index 1a27000..53395b7 100644
--- putty-0.81/windows/utils/defaults.c
+++ putty-url-0.81/windows/utils/defaults.c
@@ -5,13 +5,18 @@
 #include "putty.h"
 
 #include <commctrl.h>
+#include <versionhelpers.h>
 
 FontSpec *platform_default_fontspec(const char *name)
 {
-    if (!strcmp(name, "Font"))
+    if (!strcmp(name, "Font")) {
+    /* HACK: PuTTY-url: Set font to Consolas on Windows Vista and above */
+    if (IsWindowsVistaOrGreater()) {
+        return fontspec_new("Consolas", 0, 10, ANSI_CHARSET);
+    }
         return fontspec_new("Courier New", false, 10, ANSI_CHARSET);
-    else
-        return fontspec_new("", false, 0, 0);
+    }
+    return fontspec_new("", false, 0, 0);
 }
 
 Filename *platform_default_filename(const char *name)
diff --git putty-0.81/windows/window.c putty-url-0.81/windows/window.c
index 83600ad..a5219f1 100644
--- putty-0.81/windows/window.c
+++ putty-url-0.81/windows/window.c
@@ -31,6 +31,11 @@
 #include <richedit.h>
 #include <mmsystem.h>
 
+/*
+ * HACK: PuttyTray / Nutty
+ */
+#include "urlhack.h"
+
 /* From MSDN: In the WM_SYSCOMMAND message, the four low-order bits of
  * wParam are used by Windows, and should be masked off, so we shouldn't
  * attempt to store information in them. Hence all these identifiers have
@@ -300,6 +305,9 @@ static HICON trust_icon = INVALID_HANDLE_VALUE;
 const bool share_can_be_downstream = true;
 const bool share_can_be_upstream = true;
 
+/* HACK: PuTTY-url */
+static int urlhack_cursor_is_hand = 0;
+
 static bool is_utf8(void)
 {
     return ucsdata.line_codepage == CP_UTF8;
@@ -569,6 +577,8 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show)
 
     conf = conf_new();
 
+    urlhack_init();
+
     /*
      * Initialize COM.
      */
@@ -807,6 +817,14 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show)
     winselgui_set_hwnd(wgs.term_hwnd);
     start_backend();
 
+    /*
+     * HACK: PuttyTray / Nutty
+     * Hyperlink stuff: Set the regular expression
+     */
+    if (conf_get_bool(term->conf, CONF_url_defregex) == false) {
+        urlhack_set_regular_expression(conf_get_str(term->conf, CONF_url_regex));
+    }
+
     /*
      * Set up the initial input locale.
      */
@@ -1015,6 +1033,7 @@ void cleanup_exit(int code)
     /*
      * Clean up.
      */
+    urlhack_cleanup();
     deinit_fonts();
     sfree(logpal);
     if (pal)
@@ -2196,6 +2215,11 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
     static UINT last_mousemove = 0;
     int resize_action;
 
+    /*
+     * HACK: PuttyTray / Nutty
+     */
+    POINT cursor_pt;
+
     switch (message) {
       case WM_TIMER:
         if ((UINT_PTR)wParam == TIMING_TIMER_ID) {
@@ -2427,6 +2451,13 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
             if (backend)
                 backend_reconfig(backend, conf);
 
+            /* HACK: PuTTY-url: Reconfigure */
+            if (conf_get_bool(conf, CONF_url_defregex) == false) {
+                urlhack_set_regular_expression(conf_get_str(conf, CONF_url_regex));
+            }
+            term->url_update = TRUE;
+            term_update(term);
+
             /* Screen size changed ? */
             if (conf_get_int(conf, CONF_height) !=
                 conf_get_int(prev_conf, CONF_height) ||
@@ -2744,6 +2775,35 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
          */
         noise_ultralight(NOISE_SOURCE_MOUSEPOS, lParam);
 
+        /*
+         * HACK: PuttyTray / Nutty
+         * Hyperlink stuff: Change cursor type if hovering over link
+         */
+        if (urlhack_mouse_old_x != TO_CHR_X(X_POS(lParam)) || urlhack_mouse_old_y != TO_CHR_Y(Y_POS(lParam))) {
+            urlhack_mouse_old_x = TO_CHR_X(X_POS(lParam));
+            urlhack_mouse_old_y = TO_CHR_Y(Y_POS(lParam));
+
+            if ((!conf_get_bool(term->conf, CONF_url_ctrl_click) || urlhack_is_ctrl_pressed()) &&
+                urlhack_is_in_link_region(urlhack_mouse_old_x, urlhack_mouse_old_y)) {
+                    if (urlhack_cursor_is_hand == 0) {
+                        SetClassLongPtr(hwnd, GCLP_HCURSOR, (LONG_PTR)LoadCursor(NULL, IDC_HAND));
+                        urlhack_cursor_is_hand = 1;
+                        term_update(term); // Force the terminal to update, otherwise the underline will not show (bug somewhere, this is an ugly fix)
+                    }
+            } else if (urlhack_cursor_is_hand == 1) {
+                SetClassLongPtr(hwnd, GCLP_HCURSOR, (LONG_PTR)LoadCursor(NULL, IDC_IBEAM));
+                urlhack_cursor_is_hand = 0;
+                term_update(term); // Force the terminal to update, see above
+            }
+
+            // If mouse jumps from one link directly into another, we need a forced terminal update too
+            if (urlhack_is_in_link_region(urlhack_mouse_old_x, urlhack_mouse_old_y) != urlhack_current_region) {
+                urlhack_current_region = urlhack_is_in_link_region(urlhack_mouse_old_x, urlhack_mouse_old_y);
+                term_update(term);
+            }
+        }
+        /* HACK: PuttyTray / Nutty : END */
+
         if (wParam & (MK_LBUTTON | MK_MBUTTON | MK_RBUTTON) &&
             GetCapture() == hwnd) {
             Mouse_Button b;
@@ -3241,8 +3301,25 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
         }
         return false;
       case WM_KEYDOWN:
-      case WM_SYSKEYDOWN:
-      case WM_KEYUP:
+        /* HACK: PuTTY-url: Change cursor if we are in ctrl+click link mode (spans multiple switch cases) */
+            if (wParam == VK_CONTROL && conf_get_bool(term->conf, CONF_url_ctrl_click)) {
+                GetCursorPos(&cursor_pt);
+                ScreenToClient(hwnd, &cursor_pt);
+
+                if (urlhack_is_in_link_region(TO_CHR_X(cursor_pt.x), TO_CHR_Y(cursor_pt.y))) {
+                    SetCursor(LoadCursor(NULL, IDC_HAND));
+                    term_update(term);
+                }
+
+                goto URL_KEY_END;
+            }
+          case WM_SYSKEYDOWN:
+          case WM_KEYUP:
+            if (wParam == VK_CONTROL && conf_get_bool(term->conf, CONF_url_ctrl_click)) {
+                SetCursor(LoadCursor(NULL, IDC_IBEAM));
+                term_update(term);
+            }
+URL_KEY_END: /* HACK: PuTTY-url: End of code to change cursor when in ctrl+click link mode */
       case WM_SYSKEYUP:
         /*
          * Add the scan code and keypress timing to the random
diff --git putty-url-0.81/windows/window.c.orig putty-url-0.81/windows/window.c.orig
new file mode 100644
index 0000000..83600ad
--- /dev/null
+++ putty-url-0.81/windows/window.c.orig
@@ -0,0 +1,5898 @@
+/*
+ * window.c - the PuTTY(tel)/pterm main program, which runs a PuTTY
+ * terminal emulator and backend in a window.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <time.h>
+#include <limits.h>
+#include <assert.h>
+#include <wchar.h>
+
+#define COMPILE_MULTIMON_STUBS
+
+#include "putty.h"
+#include "ssh.h"
+#include "terminal.h"
+#include "storage.h"
+#include "putty-rc.h"
+#include "security-api.h"
+#include "win-gui-seat.h"
+#include "tree234.h"
+
+#ifdef NO_MULTIMON
+#include <multimon.h>
+#endif
+
+#include <imm.h>
+#include <commctrl.h>
+#include <richedit.h>
+#include <mmsystem.h>
+
+/* From MSDN: In the WM_SYSCOMMAND message, the four low-order bits of
+ * wParam are used by Windows, and should be masked off, so we shouldn't
+ * attempt to store information in them. Hence all these identifiers have
+ * the low 4 bits clear. Also, identifiers should < 0xF000. */
+
+#define IDM_SHOWLOG   0x0010
+#define IDM_NEWSESS   0x0020
+#define IDM_DUPSESS   0x0030
+#define IDM_RESTART   0x0040
+#define IDM_RECONF    0x0050
+#define IDM_CLRSB     0x0060
+#define IDM_RESET     0x0070
+#define IDM_HELP      0x0140
+#define IDM_ABOUT     0x0150
+#define IDM_SAVEDSESS 0x0160
+#define IDM_COPYALL   0x0170
+#define IDM_FULLSCREEN  0x0180
+#define IDM_COPY      0x0190
+#define IDM_PASTE     0x01A0
+#define IDM_SPECIALSEP 0x0200
+
+#define IDM_SPECIAL_MIN 0x0400
+#define IDM_SPECIAL_MAX 0x0800
+
+#define IDM_SAVED_MIN 0x1000
+#define IDM_SAVED_MAX 0x5000
+#define MENU_SAVED_STEP 16
+/* Maximum number of sessions on saved-session submenu */
+#define MENU_SAVED_MAX ((IDM_SAVED_MAX-IDM_SAVED_MIN) / MENU_SAVED_STEP)
+
+#define WM_IGNORE_CLIP (WM_APP + 2)
+#define WM_FULLSCR_ON_MAX (WM_APP + 3)
+#define WM_GOT_CLIPDATA (WM_APP + 4)
+
+/* Needed for Chinese support and apparently not always defined. */
+#ifndef VK_PROCESSKEY
+#define VK_PROCESSKEY 0xE5
+#endif
+
+/* Mouse wheel support. */
+#ifndef WM_MOUSEWHEEL
+#define WM_MOUSEWHEEL 0x020A           /* not defined in earlier SDKs */
+#endif
+#ifndef WM_MOUSEHWHEEL
+#define WM_MOUSEHWHEEL 0x020E          /* not defined in earlier SDKs */
+#endif
+#ifndef WHEEL_DELTA
+#define WHEEL_DELTA 120
+#endif
+
+/* DPI awareness support */
+#ifndef WM_DPICHANGED
+#define WM_DPICHANGED 0x02E0
+#define WM_DPICHANGED_BEFOREPARENT 0x02E2
+#define WM_DPICHANGED_AFTERPARENT 0x02E3
+#define WM_GETDPISCALEDSIZE 0x02E4
+#endif
+
+/* VK_PACKET, used to send Unicode characters in WM_KEYDOWNs */
+#ifndef VK_PACKET
+#define VK_PACKET 0xE7
+#endif
+
+static Mouse_Button translate_button(Mouse_Button button);
+static void show_mouseptr(bool show);
+static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
+static int TranslateKey(UINT message, WPARAM wParam, LPARAM lParam,
+                        unsigned char *output);
+static void init_palette(void);
+static void init_fonts(int, int);
+static void init_dpi_info(void);
+static void another_font(int);
+static void deinit_fonts(void);
+static void set_input_locale(HKL);
+static void update_savedsess_menu(void);
+static void init_winfuncs(void);
+
+static bool is_full_screen(void);
+static void make_full_screen(void);
+static void clear_full_screen(void);
+static void flip_full_screen(void);
+static void process_clipdata(HGLOBAL clipdata, bool unicode);
+static void setup_clipboards(Terminal *, Conf *);
+
+/* Window layout information */
+static void reset_window(int);
+static int extra_width, extra_height;
+static int font_width, font_height;
+static bool font_dualwidth, font_varpitch;
+static int offset_width, offset_height;
+static bool was_zoomed = false;
+static int prev_rows, prev_cols;
+
+static void flash_window(int mode);
+static void sys_cursor_update(void);
+static bool get_fullscreen_rect(RECT *ss);
+
+static int caret_x = -1, caret_y = -1;
+
+static int kbd_codepage;
+
+static Ldisc *ldisc;
+static Backend *backend;
+
+static cmdline_get_passwd_input_state cmdline_get_passwd_state;
+
+static struct unicode_data ucsdata;
+static bool session_closed;
+static bool reconfiguring = false;
+
+static const SessionSpecial *specials = NULL;
+static HMENU specials_menu = NULL;
+static int n_specials = 0;
+
+#define TIMING_TIMER_ID 1234
+static long timing_next_time;
+
+static struct {
+    HMENU menu;
+} popup_menus[2];
+enum { SYSMENU, CTXMENU };
+static HMENU savedsess_menu;
+
+static Conf *conf;
+static LogContext *logctx;
+static Terminal *term;
+
+static void conf_cache_data(void);
+static int cursor_type;
+static int vtmode;
+
+static struct sesslist sesslist;       /* for saved-session menu */
+
+#define FONT_NORMAL 0
+#define FONT_BOLD 1
+#define FONT_UNDERLINE 2
+#define FONT_BOLDUND 3
+#define FONT_WIDE       0x04
+#define FONT_HIGH       0x08
+#define FONT_NARROW     0x10
+
+#define FONT_OEM        0x20
+#define FONT_OEMBOLD    0x21
+#define FONT_OEMUND     0x22
+#define FONT_OEMBOLDUND 0x23
+
+#define FONT_MAXNO      0x40
+#define FONT_SHIFT      5
+static HFONT fonts[FONT_MAXNO];
+static LOGFONT lfont;
+static bool fontflag[FONT_MAXNO];
+static enum {
+    BOLD_NONE, BOLD_SHADOW, BOLD_FONT
+} bold_font_mode;
+static bool bold_colours;
+static enum {
+    UND_LINE, UND_FONT
+} und_mode;
+static int descent, font_strikethrough_y;
+
+static COLORREF colours[OSC4_NCOLOURS];
+static HPALETTE pal;
+static LPLOGPALETTE logpal;
+bool tried_pal = false;
+COLORREF colorref_modifier = 0;
+
+enum MONITOR_DPI_TYPE { MDT_EFFECTIVE_DPI, MDT_ANGULAR_DPI, MDT_RAW_DPI, MDT_DEFAULT };
+DECL_WINDOWS_FUNCTION(static, BOOL, GetMonitorInfoA, (HMONITOR, LPMONITORINFO));
+DECL_WINDOWS_FUNCTION(static, HMONITOR, MonitorFromPoint, (POINT, DWORD));
+DECL_WINDOWS_FUNCTION(static, HMONITOR, MonitorFromWindow, (HWND, DWORD));
+DECL_WINDOWS_FUNCTION(static, HRESULT, GetDpiForMonitor, (HMONITOR hmonitor, enum MONITOR_DPI_TYPE dpiType, UINT *dpiX, UINT *dpiY));
+DECL_WINDOWS_FUNCTION(static, HRESULT, GetSystemMetricsForDpi, (int nIndex, UINT dpi));
+DECL_WINDOWS_FUNCTION(static, HRESULT, AdjustWindowRectExForDpi, (LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi));
+
+static struct _dpi_info {
+    POINT cur_dpi;
+    RECT new_wnd_rect;
+} dpi_info;
+
+static HBITMAP caretbm;
+
+static int dbltime, lasttime, lastact;
+static Mouse_Button lastbtn;
+
+/* this allows xterm-style mouse handling. */
+static bool send_raw_mouse = false;
+static int wheel_accumulator = 0;
+
+static bool pointer_indicates_raw_mouse = false;
+
+static BusyStatus busy_status = BUSY_NOT;
+
+static wchar_t *window_name, *icon_name;
+
+static int compose_state = 0;
+
+static UINT wm_mousewheel = WM_MOUSEWHEEL;
+
+#define IS_HIGH_VARSEL(wch1, wch2) \
+    ((wch1) == 0xDB40 && ((wch2) >= 0xDD00 && (wch2) <= 0xDDEF))
+#define IS_LOW_VARSEL(wch) \
+    (((wch) >= 0x180B && (wch) <= 0x180D) || /* MONGOLIAN FREE VARIATION SELECTOR */ \
+     ((wch) >= 0xFE00 && (wch) <= 0xFE0F)) /* VARIATION SELECTOR 1-16 */
+
+static bool wintw_setup_draw_ctx(TermWin *);
+static void wintw_draw_text(TermWin *, int x, int y, wchar_t *text, int len,
+                            unsigned long attrs, int lattrs, truecolour tc);
+static void wintw_draw_cursor(TermWin *, int x, int y, wchar_t *text, int len,
+                              unsigned long attrs, int lattrs, truecolour tc);
+static void wintw_draw_trust_sigil(TermWin *, int x, int y);
+static int wintw_char_width(TermWin *, int uc);
+static void wintw_free_draw_ctx(TermWin *);
+static void wintw_set_cursor_pos(TermWin *, int x, int y);
+static void wintw_set_raw_mouse_mode(TermWin *, bool enable);
+static void wintw_set_raw_mouse_mode_pointer(TermWin *, bool enable);
+static void wintw_set_scrollbar(TermWin *, int total, int start, int page);
+static void wintw_bell(TermWin *, int mode);
+static void wintw_clip_write(
+    TermWin *, int clipboard, wchar_t *text, int *attrs,
+    truecolour *colours, int len, bool must_deselect);
+static void wintw_clip_request_paste(TermWin *, int clipboard);
+static void wintw_refresh(TermWin *);
+static void wintw_request_resize(TermWin *, int w, int h);
+static void wintw_set_title(TermWin *, const char *title, int codepage);
+static void wintw_set_icon_title(TermWin *, const char *icontitle,
+                                 int codepage);
+static void wintw_set_minimised(TermWin *, bool minimised);
+static void wintw_set_maximised(TermWin *, bool maximised);
+static void wintw_move(TermWin *, int x, int y);
+static void wintw_set_zorder(TermWin *, bool top);
+static void wintw_palette_set(TermWin *, unsigned, unsigned, const rgb *);
+static void wintw_palette_get_overrides(TermWin *, Terminal *);
+static void wintw_unthrottle(TermWin *win, size_t bufsize);
+
+static const TermWinVtable windows_termwin_vt = {
+    .setup_draw_ctx = wintw_setup_draw_ctx,
+    .draw_text = wintw_draw_text,
+    .draw_cursor = wintw_draw_cursor,
+    .draw_trust_sigil = wintw_draw_trust_sigil,
+    .char_width = wintw_char_width,
+    .free_draw_ctx = wintw_free_draw_ctx,
+    .set_cursor_pos = wintw_set_cursor_pos,
+    .set_raw_mouse_mode = wintw_set_raw_mouse_mode,
+    .set_raw_mouse_mode_pointer = wintw_set_raw_mouse_mode_pointer,
+    .set_scrollbar = wintw_set_scrollbar,
+    .bell = wintw_bell,
+    .clip_write = wintw_clip_write,
+    .clip_request_paste = wintw_clip_request_paste,
+    .refresh = wintw_refresh,
+    .request_resize = wintw_request_resize,
+    .set_title = wintw_set_title,
+    .set_icon_title = wintw_set_icon_title,
+    .set_minimised = wintw_set_minimised,
+    .set_maximised = wintw_set_maximised,
+    .move = wintw_move,
+    .set_zorder = wintw_set_zorder,
+    .palette_set = wintw_palette_set,
+    .palette_get_overrides = wintw_palette_get_overrides,
+    .unthrottle = wintw_unthrottle,
+};
+
+static TermWin wintw[1];
+static HDC wintw_hdc;
+
+static HICON trust_icon = INVALID_HANDLE_VALUE;
+
+const bool share_can_be_downstream = true;
+const bool share_can_be_upstream = true;
+
+static bool is_utf8(void)
+{
+    return ucsdata.line_codepage == CP_UTF8;
+}
+
+static bool win_seat_is_utf8(Seat *seat)
+{
+    return is_utf8();
+}
+
+static char *win_seat_get_ttymode(Seat *seat, const char *mode)
+{
+    return term_get_ttymode(term, mode);
+}
+
+static StripCtrlChars *win_seat_stripctrl_new(
+    Seat *seat, BinarySink *bs_out, SeatInteractionContext sic)
+{
+    return stripctrl_new_term(bs_out, false, 0, term);
+}
+
+static size_t win_seat_output(
+    Seat *seat, SeatOutputType type, const void *, size_t);
+static bool win_seat_eof(Seat *seat);
+static SeatPromptResult win_seat_get_userpass_input(Seat *seat, prompts_t *p);
+static void win_seat_notify_remote_exit(Seat *seat);
+static void win_seat_connection_fatal(Seat *seat, const char *msg);
+static void win_seat_update_specials_menu(Seat *seat);
+static void win_seat_set_busy_status(Seat *seat, BusyStatus status);
+static void win_seat_set_trust_status(Seat *seat, bool trusted);
+static bool win_seat_can_set_trust_status(Seat *seat);
+static bool win_seat_get_cursor_position(Seat *seat, int *x, int *y);
+static bool win_seat_get_window_pixel_size(Seat *seat, int *x, int *y);
+
+static const SeatVtable win_seat_vt = {
+    .output = win_seat_output,
+    .eof = win_seat_eof,
+    .sent = nullseat_sent,
+    .banner = nullseat_banner_to_stderr,
+    .get_userpass_input = win_seat_get_userpass_input,
+    .notify_session_started = nullseat_notify_session_started,
+    .notify_remote_exit = win_seat_notify_remote_exit,
+    .notify_remote_disconnect = nullseat_notify_remote_disconnect,
+    .connection_fatal = win_seat_connection_fatal,
+    .update_specials_menu = win_seat_update_specials_menu,
+    .get_ttymode = win_seat_get_ttymode,
+    .set_busy_status = win_seat_set_busy_status,
+    .confirm_ssh_host_key = win_seat_confirm_ssh_host_key,
+    .confirm_weak_crypto_primitive = win_seat_confirm_weak_crypto_primitive,
+    .confirm_weak_cached_hostkey = win_seat_confirm_weak_cached_hostkey,
+    .prompt_descriptions = win_seat_prompt_descriptions,
+    .is_utf8 = win_seat_is_utf8,
+    .echoedit_update = nullseat_echoedit_update,
+    .get_x_display = nullseat_get_x_display,
+    .get_windowid = nullseat_get_windowid,
+    .get_window_pixel_size = win_seat_get_window_pixel_size,
+    .stripctrl_new = win_seat_stripctrl_new,
+    .set_trust_status = win_seat_set_trust_status,
+    .can_set_trust_status = win_seat_can_set_trust_status,
+    .has_mixed_input_stream = nullseat_has_mixed_input_stream_yes,
+    .verbose = nullseat_verbose_yes,
+    .interactive = nullseat_interactive_yes,
+    .get_cursor_position = win_seat_get_cursor_position,
+};
+static WinGuiSeat wgs = { .seat.vt = &win_seat_vt,
+                          .logpolicy.vt = &win_gui_logpolicy_vt };
+
+static void start_backend(void)
+{
+    const struct BackendVtable *vt;
+    char *error, *realhost;
+    int i;
+
+    cmdline_get_passwd_state = cmdline_get_passwd_input_state_new;
+
+    vt = backend_vt_from_conf(conf);
+
+    seat_set_trust_status(&wgs.seat, true);
+    error = backend_init(vt, &wgs.seat, &backend, logctx, conf,
+                         conf_get_str(conf, CONF_host),
+                         conf_get_int(conf, CONF_port),
+                         &realhost,
+                         conf_get_bool(conf, CONF_tcp_nodelay),
+                         conf_get_bool(conf, CONF_tcp_keepalives));
+    if (error) {
+        char *str = dupprintf("%s Error", appname);
+        char *msg;
+        if (cmdline_tooltype & TOOLTYPE_NONNETWORK) {
+            /* Special case for pterm. */
+            msg = dupprintf("Unable to open terminal:\n%s", error);
+        } else {
+            msg = dupprintf("Unable to open connection to\n%s\n%s",
+                            conf_dest(conf), error);
+        }
+        sfree(error);
+        MessageBox(NULL, msg, str, MB_ICONERROR | MB_OK);
+        sfree(str);
+        sfree(msg);
+        exit(0);
+    }
+    term_setup_window_titles(term, realhost);
+    sfree(realhost);
+
+    /*
+     * Connect the terminal to the backend for resize purposes.
+     */
+    term_provide_backend(term, backend);
+
+    /*
+     * Set up a line discipline.
+     */
+    ldisc = ldisc_create(conf, term, backend, &wgs.seat);
+
+    /*
+     * Destroy the Restart Session menu item. (This will return
+     * failure if it's already absent, as it will be the very first
+     * time we call this function. We ignore that, because as long
+     * as the menu item ends up not being there, we don't care
+     * whether it was us who removed it or not!)
+     */
+    for (i = 0; i < lenof(popup_menus); i++) {
+        DeleteMenu(popup_menus[i].menu, IDM_RESTART, MF_BYCOMMAND);
+    }
+
+    session_closed = false;
+}
+
+static void close_session(void *ignored_context)
+{
+    char *newtitle;
+    int i;
+
+    session_closed = true;
+    newtitle = dupprintf("%s (inactive)", appname);
+    win_set_icon_title(wintw, newtitle, DEFAULT_CODEPAGE);
+    win_set_title(wintw, newtitle, DEFAULT_CODEPAGE);
+    sfree(newtitle);
+
+    if (ldisc) {
+        ldisc_free(ldisc);
+        ldisc = NULL;
+    }
+    if (backend) {
+        backend_free(backend);
+        backend = NULL;
+        term_provide_backend(term, NULL);
+        seat_update_specials_menu(&wgs.seat);
+    }
+
+    /*
+     * Show the Restart Session menu item. Do a precautionary
+     * delete first to ensure we never end up with more than one.
+     */
+    for (i = 0; i < lenof(popup_menus); i++) {
+        DeleteMenu(popup_menus[i].menu, IDM_RESTART, MF_BYCOMMAND);
+        InsertMenu(popup_menus[i].menu, IDM_DUPSESS, MF_BYCOMMAND | MF_ENABLED,
+                   IDM_RESTART, "&Restart Session");
+    }
+}
+
+/*
+ * Some machinery to deal with switching the window type between ANSI
+ * and Unicode. We prefer Unicode, but some PuTTY builds still try to
+ * run on machines so old that they don't support that mode. So we're
+ * prepared to fall back to an ANSI window if we have to. For this
+ * purpose, we swap out a few Windows API functions, and wrap
+ * SetWindowText so that if we're not in Unicode mode we first convert
+ * the wide string we're given.
+ */
+static bool unicode_window;
+static BOOL (WINAPI *sw_PeekMessage)(LPMSG, HWND, UINT, UINT, UINT);
+static LRESULT (WINAPI *sw_DispatchMessage)(const MSG *);
+static LRESULT (WINAPI *sw_DefWindowProc)(HWND, UINT, WPARAM, LPARAM);
+static void sw_SetWindowText(HWND hwnd, wchar_t *text)
+{
+    if (unicode_window) {
+        SetWindowTextW(hwnd, text);
+    } else {
+        char *mb = dup_wc_to_mb(DEFAULT_CODEPAGE, 0, text, "?");
+        SetWindowTextA(hwnd, mb);
+        sfree(mb);
+    }
+}
+
+static HINSTANCE hprev;
+
+/*
+ * Also, registering window classes has to be done in a fiddly way.
+ */
+#define SETUP_WNDCLASS(wndclass, classname) do {                        \
+        wndclass.style = 0;                                             \
+        wndclass.lpfnWndProc = WndProc;                                 \
+        wndclass.cbClsExtra = 0;                                        \
+        wndclass.cbWndExtra = 0;                                        \
+        wndclass.hInstance = hinst;                                     \
+        wndclass.hIcon = LoadIcon(hinst, MAKEINTRESOURCE(IDI_MAINICON)); \
+        wndclass.hCursor = LoadCursor(NULL, IDC_IBEAM);                 \
+        wndclass.hbrBackground = NULL;                                  \
+        wndclass.lpszMenuName = NULL;                                   \
+        wndclass.lpszClassName = classname;                             \
+    } while (0)
+wchar_t *terminal_window_class_w(void)
+{
+    static wchar_t *classname = NULL;
+    if (!classname)
+        classname = dup_mb_to_wc(DEFAULT_CODEPAGE, 0, appname);
+    if (!hprev) {
+        WNDCLASSW wndclassw;
+        SETUP_WNDCLASS(wndclassw, classname);
+        RegisterClassW(&wndclassw);
+    }
+    return classname;
+}
+char *terminal_window_class_a(void)
+{
+    static char *classname = NULL;
+    if (!classname)
+        classname = dupcat(appname, ".ansi");
+    if (!hprev) {
+        WNDCLASSA wndclassa;
+        SETUP_WNDCLASS(wndclassa, classname);
+        RegisterClassA(&wndclassa);
+    }
+    return classname;
+}
+
+HINSTANCE hinst;
+
+int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show)
+{
+    MSG msg;
+    HRESULT hr;
+    int guess_width, guess_height;
+
+    dll_hijacking_protection();
+
+    hinst = inst;
+    hprev = prev;
+
+    sk_init();
+
+    init_common_controls();
+
+    /* Set Explicit App User Model Id so that jump lists don't cause
+       PuTTY to hang on to removable media. */
+
+    set_explicit_app_user_model_id();
+
+    /* Ensure a Maximize setting in Explorer doesn't maximise the
+     * config box. */
+    defuse_showwindow();
+
+    init_winver();
+
+    /*
+     * If we're running a version of Windows that doesn't support
+     * WM_MOUSEWHEEL, find out what message number we should be
+     * using instead.
+     */
+    if (osMajorVersion < 4 ||
+        (osMajorVersion == 4 && osPlatformId != VER_PLATFORM_WIN32_NT))
+        wm_mousewheel = RegisterWindowMessage("MSWHEEL_ROLLMSG");
+
+    init_help();
+
+    init_winfuncs();
+
+    conf = conf_new();
+
+    /*
+     * Initialize COM.
+     */
+    hr = CoInitialize(NULL);
+    if (hr != S_OK && hr != S_FALSE) {
+        char *str = dupprintf("%s Fatal Error", appname);
+        MessageBox(NULL, "Failed to initialize COM subsystem",
+                   str, MB_OK | MB_ICONEXCLAMATION);
+        sfree(str);
+        return 1;
+    }
+
+    /*
+     * Process the command line.
+     */
+    gui_term_process_cmdline(conf, cmdline);
+
+    memset(&ucsdata, 0, sizeof(ucsdata));
+
+    conf_cache_data();
+
+    /*
+     * Guess some defaults for the window size. This all gets
+     * updated later, so we don't really care too much. However, we
+     * do want the font width/height guesses to correspond to a
+     * large font rather than a small one...
+     */
+
+    font_width = 10;
+    font_height = 20;
+    extra_width = 25;
+    extra_height = 28;
+    guess_width = extra_width + font_width * conf_get_int(conf, CONF_width);
+    guess_height = extra_height + font_height*conf_get_int(conf, CONF_height);
+    {
+        RECT r;
+        get_fullscreen_rect(&r);
+        if (guess_width > r.right - r.left)
+            guess_width = r.right - r.left;
+        if (guess_height > r.bottom - r.top)
+            guess_height = r.bottom - r.top;
+    }
+
+    {
+        int winmode = WS_OVERLAPPEDWINDOW | WS_VSCROLL;
+        int exwinmode = 0;
+        const struct BackendVtable *vt =
+            backend_vt_from_proto(be_default_protocol);
+        bool resize_forbidden = false;
+        if (vt && vt->flags & BACKEND_RESIZE_FORBIDDEN)
+            resize_forbidden = true;
+        wchar_t *uappname = dup_mb_to_wc(DEFAULT_CODEPAGE, 0, appname);
+        window_name = dup_mb_to_wc(DEFAULT_CODEPAGE, 0, appname);
+        icon_name = dup_mb_to_wc(DEFAULT_CODEPAGE, 0, appname);
+        if (!conf_get_bool(conf, CONF_scrollbar))
+            winmode &= ~(WS_VSCROLL);
+        if (conf_get_int(conf, CONF_resize_action) == RESIZE_DISABLED ||
+            resize_forbidden)
+            winmode &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX);
+        if (conf_get_bool(conf, CONF_alwaysontop))
+            exwinmode |= WS_EX_TOPMOST;
+        if (conf_get_bool(conf, CONF_sunken_edge))
+            exwinmode |= WS_EX_CLIENTEDGE;
+
+#ifdef TEST_ANSI_WINDOW
+        /* For developer testing of ANSI window support, pretend
+         * CreateWindowExW failed */
+        wgs.term_hwnd = NULL;
+        SetLastError(ERROR_CALL_NOT_IMPLEMENTED);
+#else
+        unicode_window = true;
+        sw_PeekMessage = PeekMessageW;
+        sw_DispatchMessage = DispatchMessageW;
+        sw_DefWindowProc = DefWindowProcW;
+        wgs.term_hwnd = CreateWindowExW(
+            exwinmode, terminal_window_class_w(), uappname,
+            winmode, CW_USEDEFAULT, CW_USEDEFAULT,
+            guess_width, guess_height, NULL, NULL, inst, NULL);
+#endif
+
+#if defined LEGACY_WINDOWS || defined TEST_ANSI_WINDOW
+        if (!wgs.term_hwnd && GetLastError() == ERROR_CALL_NOT_IMPLEMENTED) {
+            /* Fall back to an ANSI window, swapping in all the ANSI
+             * window message handling functions */
+            unicode_window = false;
+            sw_PeekMessage = PeekMessageA;
+            sw_DispatchMessage = DispatchMessageA;
+            sw_DefWindowProc = DefWindowProcA;
+            wgs.term_hwnd = CreateWindowExA(
+                exwinmode, terminal_window_class_a(), appname,
+                winmode, CW_USEDEFAULT, CW_USEDEFAULT,
+                guess_width, guess_height, NULL, NULL, inst, NULL);
+        }
+#endif
+
+        if (!wgs.term_hwnd) {
+            modalfatalbox("Unable to create terminal window: %s",
+                          win_strerror(GetLastError()));
+        }
+        memset(&dpi_info, 0, sizeof(struct _dpi_info));
+        init_dpi_info();
+        sfree(uappname);
+    }
+
+    /*
+     * Initialise the fonts, simultaneously correcting the guesses
+     * for font_{width,height}.
+     */
+    init_fonts(0,0);
+
+    /*
+     * Prepare a logical palette.
+     */
+    init_palette();
+
+    /*
+     * Initialise the terminal. (We have to do this _after_
+     * creating the window, since the terminal is the first thing
+     * which will call schedule_timer(), which will in turn call
+     * timer_change_notify() which will expect hwnd to exist.)
+     */
+    wintw->vt = &windows_termwin_vt;
+    term = term_init(conf, &ucsdata, wintw);
+    setup_clipboards(term, conf);
+    logctx = log_init(&wgs.logpolicy, conf);
+    term_provide_logctx(term, logctx);
+    term_size(term, conf_get_int(conf, CONF_height),
+              conf_get_int(conf, CONF_width),
+              conf_get_int(conf, CONF_savelines));
+
+    /*
+     * Correct the guesses for extra_{width,height}.
+     */
+    {
+        RECT cr, wr;
+        GetWindowRect(wgs.term_hwnd, &wr);
+        GetClientRect(wgs.term_hwnd, &cr);
+        offset_width = offset_height = conf_get_int(conf, CONF_window_border);
+        extra_width = wr.right - wr.left - cr.right + cr.left + offset_width*2;
+        extra_height = wr.bottom - wr.top - cr.bottom + cr.top +offset_height*2;
+    }
+
+    /*
+     * Resize the window, now we know what size we _really_ want it
+     * to be.
+     */
+    guess_width = extra_width + font_width * term->cols;
+    guess_height = extra_height + font_height * term->rows;
+    SetWindowPos(wgs.term_hwnd, NULL, 0, 0, guess_width, guess_height,
+                 SWP_NOMOVE | SWP_NOREDRAW | SWP_NOZORDER);
+
+    /*
+     * Set up a caret bitmap, with no content.
+     */
+    {
+        char *bits;
+        int size = (font_width + 15) / 16 * 2 * font_height;
+        bits = snewn(size, char);
+        memset(bits, 0, size);
+        caretbm = CreateBitmap(font_width, font_height, 1, 1, bits);
+        sfree(bits);
+    }
+    CreateCaret(wgs.term_hwnd, caretbm, font_width, font_height);
+
+    /*
+     * Initialise the scroll bar.
+     */
+    {
+        SCROLLINFO si;
+
+        si.cbSize = sizeof(si);
+        si.fMask = SIF_ALL | SIF_DISABLENOSCROLL;
+        si.nMin = 0;
+        si.nMax = term->rows - 1;
+        si.nPage = term->rows;
+        si.nPos = 0;
+        SetScrollInfo(wgs.term_hwnd, SB_VERT, &si, false);
+    }
+
+    /*
+     * Prepare the mouse handler.
+     */
+    lastact = MA_NOTHING;
+    lastbtn = MBT_NOTHING;
+    dbltime = GetDoubleClickTime();
+
+    /*
+     * Set up the session-control options on the system menu.
+     */
+    {
+        HMENU m;
+        int j;
+        char *str;
+
+        popup_menus[SYSMENU].menu = GetSystemMenu(wgs.term_hwnd, false);
+        popup_menus[CTXMENU].menu = CreatePopupMenu();
+        AppendMenu(popup_menus[CTXMENU].menu, MF_ENABLED, IDM_COPY, "&Copy");
+        AppendMenu(popup_menus[CTXMENU].menu, MF_ENABLED, IDM_PASTE, "&Paste");
+
+        savedsess_menu = CreateMenu();
+        get_sesslist(&sesslist, true);
+        update_savedsess_menu();
+
+        for (j = 0; j < lenof(popup_menus); j++) {
+            m = popup_menus[j].menu;
+
+            AppendMenu(m, MF_SEPARATOR, 0, 0);
+            AppendMenu(m, MF_ENABLED, IDM_SHOWLOG, "&Event Log");
+            AppendMenu(m, MF_SEPARATOR, 0, 0);
+            AppendMenu(m, MF_ENABLED, IDM_NEWSESS, "Ne&w Session...");
+            AppendMenu(m, MF_ENABLED, IDM_DUPSESS, "&Duplicate Session");
+            AppendMenu(m, MF_POPUP | MF_ENABLED, (UINT_PTR) savedsess_menu,
+                       "Sa&ved Sessions");
+            AppendMenu(m, MF_ENABLED, IDM_RECONF, "Chan&ge Settings...");
+            AppendMenu(m, MF_SEPARATOR, 0, 0);
+            AppendMenu(m, MF_ENABLED, IDM_COPYALL, "C&opy All to Clipboard");
+            AppendMenu(m, MF_ENABLED, IDM_CLRSB, "C&lear Scrollback");
+            AppendMenu(m, MF_ENABLED, IDM_RESET, "Rese&t Terminal");
+            AppendMenu(m, MF_SEPARATOR, 0, 0);
+            AppendMenu(m, (conf_get_int(conf, CONF_resize_action)
+                           == RESIZE_DISABLED) ? MF_GRAYED : MF_ENABLED,
+                       IDM_FULLSCREEN, "&Full Screen");
+            AppendMenu(m, MF_SEPARATOR, 0, 0);
+            if (has_help())
+                AppendMenu(m, MF_ENABLED, IDM_HELP, "&Help");
+            str = dupprintf("&About %s", appname);
+            AppendMenu(m, MF_ENABLED, IDM_ABOUT, str);
+            sfree(str);
+        }
+    }
+
+    if (restricted_acl()) {
+        lp_eventlog(&wgs.logpolicy, "Running with restricted process ACL");
+    }
+
+    winselgui_set_hwnd(wgs.term_hwnd);
+    start_backend();
+
+    /*
+     * Set up the initial input locale.
+     */
+    set_input_locale(GetKeyboardLayout(0));
+
+    /*
+     * Finally show the window!
+     */
+    ShowWindow(wgs.term_hwnd, show);
+    SetForegroundWindow(wgs.term_hwnd);
+
+    term_set_focus(term, GetForegroundWindow() == wgs.term_hwnd);
+    UpdateWindow(wgs.term_hwnd);
+
+    gui_terminal_ready(wgs.term_hwnd, &wgs.seat, backend);
+
+    while (1) {
+        int n;
+        DWORD timeout;
+
+        if (toplevel_callback_pending() ||
+            PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
+            /*
+             * If we have anything we'd like to do immediately, set
+             * the timeout for MsgWaitForMultipleObjects to zero so
+             * that we'll only do a quick check of our handles and
+             * then get on with whatever that was.
+             *
+             * One such option is a pending toplevel callback. The
+             * other is a non-empty Windows message queue, which you'd
+             * think we could leave to MsgWaitForMultipleObjects to
+             * check for us along with all the handles, but in fact we
+             * can't because once PeekMessage in one iteration of this
+             * loop has removed a message from the queue, the whole
+             * queue is considered uninteresting by the next
+             * invocation of MWFMO. So we check ourselves whether the
+             * message queue is non-empty, and if so, set this timeout
+             * to zero to ensure MWFMO doesn't block.
+             */
+            timeout = 0;
+        } else {
+            timeout = INFINITE;
+            /* The messages seem unreliable; especially if we're being tricky */
+            term_set_focus(term, GetForegroundWindow() == wgs.term_hwnd);
+        }
+
+        HandleWaitList *hwl = get_handle_wait_list();
+
+        n = MsgWaitForMultipleObjects(hwl->nhandles, hwl->handles, false,
+                                      timeout, QS_ALLINPUT);
+
+        if ((unsigned)(n - WAIT_OBJECT_0) < (unsigned)hwl->nhandles)
+            handle_wait_activate(hwl, n - WAIT_OBJECT_0);
+        handle_wait_list_free(hwl);
+
+        while (sw_PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
+            if (msg.message == WM_QUIT)
+                goto finished;         /* two-level break */
+
+            HWND logbox = event_log_window();
+            if (!(IsWindow(logbox) && IsDialogMessage(logbox, &msg)))
+                sw_DispatchMessage(&msg);
+
+            /*
+             * WM_NETEVENT messages seem to jump ahead of others in
+             * the message queue. I'm not sure why; the docs for
+             * PeekMessage mention that messages are prioritised in
+             * some way, but I'm unclear on which priorities go where.
+             *
+             * Anyway, in practice I observe that WM_NETEVENT seems to
+             * jump to the head of the queue, which means that if we
+             * were to only process one message every time round this
+             * loop, we'd get nothing but NETEVENTs if the server
+             * flooded us with data, and stop responding to any other
+             * kind of window message. So instead, we keep on round
+             * this loop until we've consumed at least one message
+             * that _isn't_ a NETEVENT, or run out of messages
+             * completely (whichever comes first). And we don't go to
+             * run_toplevel_callbacks (which is where the netevents
+             * are actually processed, causing fresh NETEVENT messages
+             * to appear) until we've done this.
+             */
+            if (msg.message != WM_NETEVENT)
+                break;
+        }
+
+        run_toplevel_callbacks();
+    }
+
+  finished:
+    cleanup_exit(msg.wParam);          /* this doesn't return... */
+    return msg.wParam;                 /* ... but optimiser doesn't know */
+}
+
+char *handle_restrict_acl_cmdline_prefix(char *p)
+{
+    /*
+     * Process the &R prefix on a command line, which is equivalent to
+     * -restrict-acl but lexically easier to prepend when another
+     * instance of ourself automatically constructs a command line.
+     *
+     * If successful, restricts the process ACL and advances the input
+     * pointer past the prefix. Returns the updated pointer (whether
+     * it moved or not).
+     */
+    while (*p && isspace((unsigned char)*p))
+        p++;
+    if (*p == '&' && p[1] == 'R' &&
+        (!p[2] || p[2] == '@' || p[2] == '&')) {
+        /* &R restrict-acl prefix */
+        restrict_process_acl();
+        p += 2;
+    }
+    return p;
+}
+
+bool handle_special_sessionname_cmdline(char *p, Conf *conf)
+{
+    /*
+     * Process the special form of command line with an initial @
+     * followed by the name of a saved session with _no quoting or
+     * escaping_. This is a very convenient means of automated
+     * saved-session launching, via IDM_SAVEDSESS or Windows 7 jump
+     * lists.
+     *
+     * If successful, the whole command line has been interpreted in
+     * this way, so there's nothing left to parse into other arguments.
+     */
+    if (*p != '@')
+        return false;
+
+    ptrlen sessionname = ptrlen_from_asciz(p + 1);
+    while (sessionname.len > 0 &&
+           isspace(((unsigned char *)sessionname.ptr)[sessionname.len-1]))
+        sessionname.len--;
+
+    char *dup = mkstr(sessionname);
+    bool loaded = do_defaults(dup, conf);
+    sfree(dup);
+
+    return loaded;
+}
+
+bool handle_special_filemapping_cmdline(char *p, Conf *conf)
+{
+    /*
+     * Process the special form of command line with an initial &
+     * followed by the hex value of a HANDLE for a file mapping object
+     * and the size of the data contained in it, which we must
+     * interpret as a serialised Conf.
+     *
+     * If successful, the whole command line has been interpreted in
+     * this way, so there's nothing left to parse into other arguments.
+     */
+
+    if (*p != '&')
+        return false;
+
+    HANDLE filemap;
+    unsigned cpsize;
+    if (sscanf(p + 1, "%p:%u", &filemap, &cpsize) != 2)
+        return false;
+
+    void *cp = MapViewOfFile(filemap, FILE_MAP_READ, 0, 0, cpsize);
+    if (!cp)
+        return false;
+
+    BinarySource src[1];
+    BinarySource_BARE_INIT(src, cp, cpsize);
+    if (!conf_deserialise(conf, src))
+        modalfatalbox("Serialised configuration data was invalid");
+    UnmapViewOfFile(cp);
+    CloseHandle(filemap);
+    return true;
+}
+
+static void setup_clipboards(Terminal *term, Conf *conf)
+{
+    assert(term->mouse_select_clipboards[0] == CLIP_LOCAL);
+
+    term->n_mouse_select_clipboards = 1;
+
+    if (conf_get_bool(conf, CONF_mouseautocopy)) {
+        term->mouse_select_clipboards[
+            term->n_mouse_select_clipboards++] = CLIP_SYSTEM;
+    }
+
+    switch (conf_get_int(conf, CONF_mousepaste)) {
+      case CLIPUI_IMPLICIT:
+        term->mouse_paste_clipboard = CLIP_LOCAL;
+        break;
+      case CLIPUI_EXPLICIT:
+        term->mouse_paste_clipboard = CLIP_SYSTEM;
+        break;
+      default:
+        term->mouse_paste_clipboard = CLIP_NULL;
+        break;
+    }
+}
+
+/*
+ * Clean up and exit.
+ */
+void cleanup_exit(int code)
+{
+    /*
+     * Clean up.
+     */
+    deinit_fonts();
+    sfree(logpal);
+    if (pal)
+        DeleteObject(pal);
+    sk_cleanup();
+
+    random_save_seed();
+    shutdown_help();
+
+    /* Clean up COM. */
+    CoUninitialize();
+
+    exit(code);
+}
+
+/*
+ * Refresh the saved-session submenu from `sesslist'.
+ */
+static void update_savedsess_menu(void)
+{
+    int i;
+    while (DeleteMenu(savedsess_menu, 0, MF_BYPOSITION)) ;
+    /* skip sesslist.sessions[0] == Default Settings */
+    for (i = 1;
+         i < ((sesslist.nsessions <= MENU_SAVED_MAX+1) ? sesslist.nsessions
+                                                       : MENU_SAVED_MAX+1);
+         i++)
+        AppendMenu(savedsess_menu, MF_ENABLED,
+                   IDM_SAVED_MIN + (i-1)*MENU_SAVED_STEP,
+                   sesslist.sessions[i]);
+    if (sesslist.nsessions <= 1)
+        AppendMenu(savedsess_menu, MF_GRAYED, IDM_SAVED_MIN, "(No sessions)");
+}
+
+/*
+ * Update the Special Commands submenu.
+ */
+static void win_seat_update_specials_menu(Seat *seat)
+{
+    HMENU new_menu;
+    int i, j;
+
+    if (backend)
+        specials = backend_get_specials(backend);
+    else
+        specials = NULL;
+
+    if (specials) {
+        /* We can't use Windows to provide a stack for submenus, so
+         * here's a lame "stack" that will do for now. */
+        HMENU saved_menu = NULL;
+        int nesting = 1;
+        new_menu = CreatePopupMenu();
+        for (i = 0; nesting > 0; i++) {
+            assert(IDM_SPECIAL_MIN + 0x10 * i < IDM_SPECIAL_MAX);
+            switch (specials[i].code) {
+              case SS_SEP:
+                AppendMenu(new_menu, MF_SEPARATOR, 0, 0);
+                break;
+              case SS_SUBMENU:
+                assert(nesting < 2);
+                nesting++;
+                saved_menu = new_menu; /* XXX lame stacking */
+                new_menu = CreatePopupMenu();
+                AppendMenu(saved_menu, MF_POPUP | MF_ENABLED,
+                           (UINT_PTR) new_menu, specials[i].name);
+                break;
+              case SS_EXITMENU:
+                nesting--;
+                if (nesting) {
+                    new_menu = saved_menu; /* XXX lame stacking */
+                    saved_menu = NULL;
+                }
+                break;
+              default:
+                AppendMenu(new_menu, MF_ENABLED, IDM_SPECIAL_MIN + 0x10 * i,
+                           specials[i].name);
+                break;
+            }
+        }
+        /* Squirrel the highest special. */
+        n_specials = i - 1;
+    } else {
+        new_menu = NULL;
+        n_specials = 0;
+    }
+
+    for (j = 0; j < lenof(popup_menus); j++) {
+        if (specials_menu) {
+            /* XXX does this free up all submenus? */
+            DeleteMenu(popup_menus[j].menu, (UINT_PTR)specials_menu,
+                       MF_BYCOMMAND);
+            DeleteMenu(popup_menus[j].menu, IDM_SPECIALSEP, MF_BYCOMMAND);
+        }
+        if (new_menu) {
+            InsertMenu(popup_menus[j].menu, IDM_SHOWLOG,
+                       MF_BYCOMMAND | MF_POPUP | MF_ENABLED,
+                       (UINT_PTR) new_menu, "S&pecial Command");
+            InsertMenu(popup_menus[j].menu, IDM_SHOWLOG,
+                       MF_BYCOMMAND | MF_SEPARATOR, IDM_SPECIALSEP, 0);
+        }
+    }
+    specials_menu = new_menu;
+}
+
+static void update_mouse_pointer(void)
+{
+    LPTSTR curstype = NULL;
+    bool force_visible = false;
+    static bool forced_visible = false;
+    switch (busy_status) {
+      case BUSY_NOT:
+        if (pointer_indicates_raw_mouse)
+            curstype = IDC_ARROW;
+        else
+            curstype = IDC_IBEAM;
+        break;
+      case BUSY_WAITING:
+        curstype = IDC_APPSTARTING; /* this may be an abuse */
+        force_visible = true;
+        break;
+      case BUSY_CPU:
+        curstype = IDC_WAIT;
+        force_visible = true;
+        break;
+      default:
+        unreachable("Bad busy_status");
+    }
+    {
+        HCURSOR cursor = LoadCursor(NULL, curstype);
+        SetClassLongPtr(wgs.term_hwnd, GCLP_HCURSOR, (LONG_PTR)cursor);
+        SetCursor(cursor); /* force redraw of cursor at current posn */
+    }
+    if (force_visible != forced_visible) {
+        /* We want some cursor shapes to be visible always.
+         * Along with show_mouseptr(), this manages the ShowCursor()
+         * counter such that if we switch back to a non-force_visible
+         * cursor, the previous visibility state is restored. */
+        ShowCursor(force_visible);
+        forced_visible = force_visible;
+    }
+}
+
+static void win_seat_set_busy_status(Seat *seat, BusyStatus status)
+{
+    busy_status = status;
+    update_mouse_pointer();
+}
+
+static void wintw_set_raw_mouse_mode(TermWin *tw, bool activate)
+{
+    send_raw_mouse = activate;
+}
+
+static void wintw_set_raw_mouse_mode_pointer(TermWin *tw, bool activate)
+{
+    pointer_indicates_raw_mouse = activate;
+    update_mouse_pointer();
+}
+
+/*
+ * Print a message box and close the connection.
+ */
+static void win_seat_connection_fatal(Seat *seat, const char *msg)
+{
+    char *title = dupprintf("%s Fatal Error", appname);
+    show_mouseptr(true);
+    MessageBox(wgs.term_hwnd, msg, title, MB_ICONERROR | MB_OK);
+    sfree(title);
+
+    if (conf_get_int(conf, CONF_close_on_exit) == FORCE_ON)
+        PostQuitMessage(1);
+    else {
+        queue_toplevel_callback(close_session, NULL);
+    }
+}
+
+/*
+ * Report an error at the command-line parsing stage.
+ */
+void cmdline_error(const char *fmt, ...)
+{
+    va_list ap;
+    char *message, *title;
+
+    va_start(ap, fmt);
+    message = dupvprintf(fmt, ap);
+    va_end(ap);
+    title = dupprintf("%s Command Line Error", appname);
+    MessageBox(wgs.term_hwnd, message, title, MB_ICONERROR | MB_OK);
+    sfree(message);
+    sfree(title);
+    exit(1);
+}
+
+static inline rgb rgb_from_colorref(COLORREF cr)
+{
+    rgb toret;
+    toret.r = GetRValue(cr);
+    toret.g = GetGValue(cr);
+    toret.b = GetBValue(cr);
+    return toret;
+}
+
+static void wintw_palette_get_overrides(TermWin *tw, Terminal *term)
+{
+    if (conf_get_bool(conf, CONF_system_colour)) {
+        rgb rgb;
+
+        rgb = rgb_from_colorref(GetSysColor(COLOR_WINDOWTEXT));
+        term_palette_override(term, OSC4_COLOUR_fg, rgb);
+        term_palette_override(term, OSC4_COLOUR_fg_bold, rgb);
+
+        rgb = rgb_from_colorref(GetSysColor(COLOR_WINDOW));
+        term_palette_override(term, OSC4_COLOUR_bg, rgb);
+        term_palette_override(term, OSC4_COLOUR_bg_bold, rgb);
+
+        rgb = rgb_from_colorref(GetSysColor(COLOR_HIGHLIGHTTEXT));
+        term_palette_override(term, OSC4_COLOUR_cursor_fg, rgb);
+
+        rgb = rgb_from_colorref(GetSysColor(COLOR_HIGHLIGHT));
+        term_palette_override(term, OSC4_COLOUR_cursor_bg, rgb);
+    }
+}
+
+/*
+ * This is a wrapper to ExtTextOut() to force Windows to display
+ * the precise glyphs we give it. Otherwise it would do its own
+ * bidi and Arabic shaping, and we would end up uncertain which
+ * characters it had put where.
+ */
+static void exact_textout(HDC hdc, int x, int y, CONST RECT *lprc,
+                          unsigned short *lpString, UINT cbCount,
+                          CONST INT *lpDx, bool opaque)
+{
+#if HAVE_GCP_RESULTSW
+    GCP_RESULTSW gcpr;
+#else
+    /*
+     * If building against old enough headers that the GCP_RESULTSW
+     * type isn't available, we can make do with GCP_RESULTS proper:
+     * the differences aren't important to us (the only variable-width
+     * string parameter is one we don't use anyway).
+     */
+    GCP_RESULTS gcpr;
+#endif
+    char *buffer = snewn(cbCount*2+2, char);
+    char *classbuffer = snewn(cbCount, char);
+    memset(&gcpr, 0, sizeof(gcpr));
+    memset(buffer, 0, cbCount*2+2);
+    memset(classbuffer, GCPCLASS_NEUTRAL, cbCount);
+
+    gcpr.lStructSize = sizeof(gcpr);
+    gcpr.lpGlyphs = (void *)buffer;
+    gcpr.lpClass = (void *)classbuffer;
+    gcpr.nGlyphs = cbCount;
+    GetCharacterPlacementW(hdc, lpString, cbCount, 0, &gcpr,
+                           FLI_MASK | GCP_CLASSIN | GCP_DIACRITIC);
+
+    ExtTextOut(hdc, x, y,
+               ETO_GLYPH_INDEX | ETO_CLIPPED | (opaque ? ETO_OPAQUE : 0),
+               lprc, buffer, cbCount, lpDx);
+}
+
+/*
+ * The exact_textout() wrapper, unfortunately, destroys the useful
+ * Windows `font linking' behaviour: automatic handling of Unicode
+ * code points not supported in this font by falling back to a font
+ * which does contain them. Therefore, we adopt a multi-layered
+ * approach: for any potentially-bidi text, we use exact_textout(),
+ * and for everything else we use a simple ExtTextOut as we did
+ * before exact_textout() was introduced.
+ */
+static void general_textout(HDC hdc, int x, int y, CONST RECT *lprc,
+                            unsigned short *lpString, UINT cbCount,
+                            CONST INT *lpDx, bool opaque)
+{
+    int i, j, xp, xn;
+    int bkmode = 0;
+    bool got_bkmode = false;
+
+    xp = xn = x;
+
+    for (i = 0; i < (int)cbCount ;) {
+        bool rtl = is_rtl(lpString[i]);
+
+        xn += lpDx[i];
+
+        for (j = i+1; j < (int)cbCount; j++) {
+            if (rtl != is_rtl(lpString[j]))
+                break;
+            xn += lpDx[j];
+        }
+
+        /*
+         * Now [i,j) indicates a maximal substring of lpString
+         * which should be displayed using the same textout
+         * function.
+         */
+        if (rtl) {
+            exact_textout(hdc, xp, y, lprc, lpString+i, j-i,
+                          font_varpitch ? NULL : lpDx+i, opaque);
+        } else {
+            ExtTextOutW(hdc, xp, y, ETO_CLIPPED | (opaque ? ETO_OPAQUE : 0),
+                        lprc, lpString+i, j-i,
+                        font_varpitch ? NULL : lpDx+i);
+        }
+
+        i = j;
+        xp = xn;
+
+        bkmode = GetBkMode(hdc);
+        got_bkmode = true;
+        SetBkMode(hdc, TRANSPARENT);
+        opaque = false;
+    }
+
+    if (got_bkmode)
+        SetBkMode(hdc, bkmode);
+}
+
+static int get_font_width(HDC hdc, const TEXTMETRIC *tm)
+{
+    int ret;
+    /* Note that the TMPF_FIXED_PITCH bit is defined upside down :-( */
+    if (!(tm->tmPitchAndFamily & TMPF_FIXED_PITCH)) {
+        ret = tm->tmAveCharWidth;
+    } else {
+#define FIRST '0'
+#define LAST '9'
+        ABCFLOAT widths[LAST-FIRST + 1];
+        int j;
+
+        font_varpitch = true;
+        font_dualwidth = true;
+        if (GetCharABCWidthsFloat(hdc, FIRST, LAST, widths)) {
+            ret = 0;
+            for (j = 0; j < lenof(widths); j++) {
+                int width = (int)(0.5 + widths[j].abcfA +
+                                  widths[j].abcfB + widths[j].abcfC);
+                if (ret < width)
+                    ret = width;
+            }
+        } else {
+            ret = tm->tmMaxCharWidth;
+        }
+#undef FIRST
+#undef LAST
+    }
+    return ret;
+}
+
+static void init_dpi_info(void)
+{
+    if (dpi_info.cur_dpi.x == 0 || dpi_info.cur_dpi.y == 0) {
+        if (p_GetDpiForMonitor && p_MonitorFromWindow) {
+            UINT dpiX, dpiY;
+            HMONITOR currentMonitor = p_MonitorFromWindow(
+                wgs.term_hwnd, MONITOR_DEFAULTTOPRIMARY);
+            if (p_GetDpiForMonitor(currentMonitor, MDT_EFFECTIVE_DPI,
+                                   &dpiX, &dpiY) == S_OK) {
+                dpi_info.cur_dpi.x = (int)dpiX;
+                dpi_info.cur_dpi.y = (int)dpiY;
+            }
+        }
+
+        /* Fall back to system DPI */
+        if (dpi_info.cur_dpi.x == 0 || dpi_info.cur_dpi.y == 0) {
+            HDC hdc = GetDC(wgs.term_hwnd);
+            dpi_info.cur_dpi.x = GetDeviceCaps(hdc, LOGPIXELSX);
+            dpi_info.cur_dpi.y = GetDeviceCaps(hdc, LOGPIXELSY);
+            ReleaseDC(wgs.term_hwnd, hdc);
+        }
+    }
+}
+
+/*
+ * Initialise all the fonts we will need initially. There may be as many as
+ * three or as few as one.  The other (potentially) twenty-one fonts are done
+ * if/when they are needed.
+ *
+ * We also:
+ *
+ * - check the font width and height, correcting our guesses if
+ *   necessary.
+ *
+ * - verify that the bold font is the same width as the ordinary
+ *   one, and engage shadow bolding if not.
+ *
+ * - verify that the underlined font is the same width as the
+ *   ordinary one (manual underlining by means of line drawing can
+ *   be done in a pinch).
+ *
+ * - find a trust sigil icon that will look OK with the chosen font.
+ */
+static void init_fonts(int pick_width, int pick_height)
+{
+    TEXTMETRIC tm;
+    OUTLINETEXTMETRIC otm;
+    CPINFO cpinfo;
+    FontSpec *font;
+    int fontsize[3];
+    int i;
+    int quality;
+    HDC hdc;
+    int fw_dontcare, fw_bold;
+
+    for (i = 0; i < FONT_MAXNO; i++)
+        fonts[i] = NULL;
+
+    bold_font_mode = conf_get_int(conf, CONF_bold_style) & 1 ?
+        BOLD_FONT : BOLD_NONE;
+    bold_colours = conf_get_int(conf, CONF_bold_style) & 2 ? true : false;
+    und_mode = UND_FONT;
+
+    font = conf_get_fontspec(conf, CONF_font);
+    if (font->isbold) {
+        fw_dontcare = FW_BOLD;
+        fw_bold = FW_HEAVY;
+    } else {
+        fw_dontcare = FW_DONTCARE;
+        fw_bold = FW_BOLD;
+    }
+
+    hdc = GetDC(wgs.term_hwnd);
+
+    if (pick_height)
+        font_height = pick_height;
+    else {
+        font_height = font->height;
+        if (font_height > 0) {
+            font_height =
+                -MulDiv(font_height, dpi_info.cur_dpi.y, 72);
+        }
+    }
+    font_width = pick_width;
+
+    quality = conf_get_int(conf, CONF_font_quality);
+#define f(i,c,w,u) \
+    fonts[i] = CreateFont (font_height, font_width, 0, 0, w, false, u, false, \
+                           c, OUT_DEFAULT_PRECIS, \
+                           CLIP_DEFAULT_PRECIS, FONT_QUALITY(quality), \
+                           FIXED_PITCH | FF_DONTCARE, font->name)
+
+    f(FONT_NORMAL, font->charset, fw_dontcare, false);
+
+    SelectObject(hdc, fonts[FONT_NORMAL]);
+    GetTextMetrics(hdc, &tm);
+    if (GetOutlineTextMetrics(hdc, sizeof(otm), &otm))
+        font_strikethrough_y = tm.tmAscent - otm.otmsStrikeoutPosition;
+    else
+        font_strikethrough_y = tm.tmAscent - (tm.tmAscent * 3 / 8);
+
+    GetObject(fonts[FONT_NORMAL], sizeof(LOGFONT), &lfont);
+
+    /* Note that the TMPF_FIXED_PITCH bit is defined upside down :-( */
+    if (!(tm.tmPitchAndFamily & TMPF_FIXED_PITCH)) {
+        font_varpitch = false;
+        font_dualwidth = (tm.tmAveCharWidth != tm.tmMaxCharWidth);
+    } else {
+        font_varpitch = true;
+        font_dualwidth = true;
+    }
+    if (pick_width == 0 || pick_height == 0) {
+        font_height = tm.tmHeight;
+        font_width = get_font_width(hdc, &tm);
+    }
+
+#ifdef RDB_DEBUG_PATCH
+    debug("Primary font H=%d, AW=%d, MW=%d\n",
+          tm.tmHeight, tm.tmAveCharWidth, tm.tmMaxCharWidth);
+#endif
+
+    {
+        CHARSETINFO info;
+        DWORD cset = tm.tmCharSet;
+        memset(&info, 0xFF, sizeof(info));
+
+        /* !!! Yes the next line is right */
+        if (cset == OEM_CHARSET)
+            ucsdata.font_codepage = GetOEMCP();
+        else if (TranslateCharsetInfo ((DWORD *)(ULONG_PTR)cset,
+                                       &info, TCI_SRCCHARSET))
+            ucsdata.font_codepage = info.ciACP;
+        else
+            ucsdata.font_codepage = -1;
+
+        GetCPInfo(ucsdata.font_codepage, &cpinfo);
+        ucsdata.dbcs_screenfont = (cpinfo.MaxCharSize > 1);
+    }
+
+    f(FONT_UNDERLINE, font->charset, fw_dontcare, true);
+
+    /*
+     * Some fonts, e.g. 9-pt Courier, draw their underlines
+     * outside their character cell. We successfully prevent
+     * screen corruption by clipping the text output, but then
+     * we lose the underline completely. Here we try to work
+     * out whether this is such a font, and if it is, we set a
+     * flag that causes underlines to be drawn by hand.
+     *
+     * Having tried other more sophisticated approaches (such
+     * as examining the TEXTMETRIC structure or requesting the
+     * height of a string), I think we'll do this the brute
+     * force way: we create a small bitmap, draw an underlined
+     * space on it, and test to see whether any pixels are
+     * foreground-coloured. (Since we expect the underline to
+     * go all the way across the character cell, we only search
+     * down a single column of the bitmap, half way across.)
+     */
+    {
+        HDC und_dc;
+        HBITMAP und_bm, und_oldbm;
+        int i;
+        bool gotit;
+        COLORREF c;
+
+        und_dc = CreateCompatibleDC(hdc);
+        und_bm = CreateCompatibleBitmap(hdc, font_width, font_height);
+        und_oldbm = SelectObject(und_dc, und_bm);
+        SelectObject(und_dc, fonts[FONT_UNDERLINE]);
+        SetTextAlign(und_dc, TA_TOP | TA_LEFT | TA_NOUPDATECP);
+        SetTextColor(und_dc, RGB(255, 255, 255));
+        SetBkColor(und_dc, RGB(0, 0, 0));
+        SetBkMode(und_dc, OPAQUE);
+        ExtTextOut(und_dc, 0, 0, ETO_OPAQUE, NULL, " ", 1, NULL);
+        gotit = false;
+        for (i = 0; i < font_height; i++) {
+            c = GetPixel(und_dc, font_width / 2, i);
+            if (c != RGB(0, 0, 0))
+                gotit = true;
+        }
+        SelectObject(und_dc, und_oldbm);
+        DeleteObject(und_bm);
+        DeleteDC(und_dc);
+        if (!gotit) {
+            und_mode = UND_LINE;
+            DeleteObject(fonts[FONT_UNDERLINE]);
+            fonts[FONT_UNDERLINE] = 0;
+        }
+    }
+
+    if (bold_font_mode == BOLD_FONT) {
+        f(FONT_BOLD, font->charset, fw_bold, false);
+    }
+#undef f
+
+    descent = tm.tmAscent + 1;
+    if (descent >= font_height)
+        descent = font_height - 1;
+
+    for (i = 0; i < 3; i++) {
+        if (fonts[i]) {
+            if (SelectObject(hdc, fonts[i]) && GetTextMetrics(hdc, &tm))
+                fontsize[i] = get_font_width(hdc, &tm) + 256 * tm.tmHeight;
+            else
+                fontsize[i] = -i;
+        } else
+            fontsize[i] = -i;
+    }
+
+    ReleaseDC(wgs.term_hwnd, hdc);
+
+    if (trust_icon != INVALID_HANDLE_VALUE) {
+        DestroyIcon(trust_icon);
+    }
+    trust_icon = LoadImage(hinst, MAKEINTRESOURCE(IDI_MAINICON),
+                           IMAGE_ICON, font_width*2, font_height,
+                           LR_DEFAULTCOLOR);
+
+    if (fontsize[FONT_UNDERLINE] != fontsize[FONT_NORMAL]) {
+        und_mode = UND_LINE;
+        DeleteObject(fonts[FONT_UNDERLINE]);
+        fonts[FONT_UNDERLINE] = 0;
+    }
+
+    if (bold_font_mode == BOLD_FONT &&
+        fontsize[FONT_BOLD] != fontsize[FONT_NORMAL]) {
+        bold_font_mode = BOLD_SHADOW;
+        DeleteObject(fonts[FONT_BOLD]);
+        fonts[FONT_BOLD] = 0;
+    }
+    fontflag[0] = true;
+    fontflag[1] = true;
+    fontflag[2] = true;
+
+    init_ucs(conf, &ucsdata);
+}
+
+static void another_font(int fontno)
+{
+    int basefont;
+    int fw_dontcare, fw_bold, quality;
+    int c, w, x;
+    bool u;
+    char *s;
+    FontSpec *font;
+
+    if (fontno < 0 || fontno >= FONT_MAXNO || fontflag[fontno])
+        return;
+
+    basefont = (fontno & ~(FONT_BOLDUND));
+    if (basefont != fontno && !fontflag[basefont])
+        another_font(basefont);
+
+    font = conf_get_fontspec(conf, CONF_font);
+
+    if (font->isbold) {
+        fw_dontcare = FW_BOLD;
+        fw_bold = FW_HEAVY;
+    } else {
+        fw_dontcare = FW_DONTCARE;
+        fw_bold = FW_BOLD;
+    }
+
+    c = font->charset;
+    w = fw_dontcare;
+    u = false;
+    s = font->name;
+    x = font_width;
+
+    if (fontno & FONT_WIDE)
+        x *= 2;
+    if (fontno & FONT_NARROW)
+        x = (x+1)/2;
+    if (fontno & FONT_OEM)
+        c = OEM_CHARSET;
+    if (fontno & FONT_BOLD)
+        w = fw_bold;
+    if (fontno & FONT_UNDERLINE)
+        u = true;
+
+    quality = conf_get_int(conf, CONF_font_quality);
+
+    fonts[fontno] =
+        CreateFont(font_height * (1 + !!(fontno & FONT_HIGH)), x, 0, 0, w,
+                   false, u, false, c, OUT_DEFAULT_PRECIS,
+                   CLIP_DEFAULT_PRECIS, FONT_QUALITY(quality),
+                   DEFAULT_PITCH | FF_DONTCARE, s);
+
+    fontflag[fontno] = true;
+}
+
+static void deinit_fonts(void)
+{
+    int i;
+    for (i = 0; i < FONT_MAXNO; i++) {
+        if (fonts[i])
+            DeleteObject(fonts[i]);
+        fonts[i] = 0;
+        fontflag[i] = false;
+    }
+
+    if (trust_icon != INVALID_HANDLE_VALUE) {
+        DestroyIcon(trust_icon);
+    }
+    trust_icon = INVALID_HANDLE_VALUE;
+}
+
+static void wintw_request_resize(TermWin *tw, int w, int h)
+{
+    const struct BackendVtable *vt;
+    int width, height;
+    int resize_action = conf_get_int(conf, CONF_resize_action);
+    bool deny_resize = false;
+
+    /* Suppress server-originated resizing attempts if local resizing
+     * is disabled entirely, or if it's supposed to change
+     * rows/columns but the window is maximised. */
+    if (resize_action == RESIZE_DISABLED
+        || (resize_action == RESIZE_TERM && IsZoomed(wgs.term_hwnd))) {
+        deny_resize = true;
+    }
+
+    vt = backend_vt_from_proto(be_default_protocol);
+    if (vt && vt->flags & BACKEND_RESIZE_FORBIDDEN)
+        deny_resize = true;
+    if (h == term->rows && w == term->cols) deny_resize = true;
+
+    /* We still need to acknowledge a suppressed resize attempt. */
+    if (deny_resize) {
+        term_resize_request_completed(term);
+        return;
+    }
+
+    /* Sanity checks ... */
+    {
+        RECT ss;
+        if (get_fullscreen_rect(&ss)) {
+            /* Make sure the values aren't too big */
+            width = (ss.right - ss.left - extra_width) / 4;
+            height = (ss.bottom - ss.top - extra_height) / 6;
+
+            if (w > width || h > height) {
+                term_resize_request_completed(term);
+                return;
+            }
+            if (w < 15)
+                w = 15;
+            if (h < 1)
+                h = 1;
+        }
+    }
+
+    if (resize_action != RESIZE_FONT && !IsZoomed(wgs.term_hwnd)) {
+        width = extra_width + font_width * w;
+        height = extra_height + font_height * h;
+
+        SetWindowPos(wgs.term_hwnd, NULL, 0, 0, width, height,
+                     SWP_NOACTIVATE | SWP_NOCOPYBITS |
+                     SWP_NOMOVE | SWP_NOZORDER);
+    } else {
+        /*
+         * If we're resizing by changing the font, we must tell the
+         * terminal the new size immediately, so that reset_window
+         * will know what to do.
+         */
+        term_size(term, h, w, conf_get_int(conf, CONF_savelines));
+        reset_window(0);
+    }
+
+    term_resize_request_completed(term);
+    InvalidateRect(wgs.term_hwnd, NULL, true);
+}
+
+static void recompute_window_offset(void)
+{
+    RECT cr;
+    GetClientRect(wgs.term_hwnd, &cr);
+
+    int win_width  = cr.right - cr.left;
+    int win_height = cr.bottom - cr.top;
+
+    int new_offset_width = (win_width-font_width*term->cols)/2;
+    int new_offset_height = (win_height-font_height*term->rows)/2;
+
+    if (offset_width != new_offset_width ||
+        offset_height != new_offset_height) {
+        offset_width = new_offset_width;
+        offset_height = new_offset_height;
+        InvalidateRect(wgs.term_hwnd, NULL, true);
+    }
+}
+
+static void reset_window(int reinit) {
+    /*
+     * This function decides how to resize or redraw when the
+     * user changes something.
+     *
+     * This function doesn't like to change the terminal size but if the
+     * font size is locked that may be it's only soluion.
+     */
+    int win_width, win_height, resize_action, window_border;
+    RECT cr, wr;
+
+#ifdef RDB_DEBUG_PATCH
+    debug("reset_window()\n");
+#endif
+
+    /* Current window sizes ... */
+    GetWindowRect(wgs.term_hwnd, &wr);
+    GetClientRect(wgs.term_hwnd, &cr);
+
+    win_width  = cr.right - cr.left;
+    win_height = cr.bottom - cr.top;
+
+    resize_action = conf_get_int(conf, CONF_resize_action);
+    window_border = conf_get_int(conf, CONF_window_border);
+
+    if (resize_action == RESIZE_DISABLED)
+        reinit = 2;
+
+    /* Are we being forced to reload the fonts ? */
+    if (reinit>1) {
+#ifdef RDB_DEBUG_PATCH
+        debug("reset_window() -- Forced deinit\n");
+#endif
+        deinit_fonts();
+        init_fonts(0,0);
+    }
+
+    /* Oh, looks like we're minimised */
+    if (win_width == 0 || win_height == 0)
+        return;
+
+    /* Is the window out of position ? */
+    if (!reinit) {
+        recompute_window_offset();
+#ifdef RDB_DEBUG_PATCH
+        debug("reset_window() -> Reposition terminal\n");
+#endif
+    }
+
+    if (IsZoomed(wgs.term_hwnd)) {
+        /* We're fullscreen, this means we must not change the size of
+         * the window so it's the font size or the terminal itself.
+         */
+
+        extra_width = wr.right - wr.left - cr.right + cr.left;
+        extra_height = wr.bottom - wr.top - cr.bottom + cr.top;
+
+        if (resize_action != RESIZE_TERM) {
+            if (font_width != win_width/term->cols ||
+                font_height != win_height/term->rows) {
+                deinit_fonts();
+                init_fonts(win_width/term->cols, win_height/term->rows);
+                offset_width = (win_width-font_width*term->cols)/2;
+                offset_height = (win_height-font_height*term->rows)/2;
+                InvalidateRect(wgs.term_hwnd, NULL, true);
+#ifdef RDB_DEBUG_PATCH
+                debug("reset_window() -> Z font resize to (%d, %d)\n",
+                      font_width, font_height);
+#endif
+            }
+        } else {
+            if (font_width * term->cols != win_width ||
+                font_height * term->rows != win_height) {
+                /* Our only choice at this point is to change the
+                 * size of the terminal; Oh well.
+                 */
+                term_size(term, win_height/font_height, win_width/font_width,
+                          conf_get_int(conf, CONF_savelines));
+                offset_width = (win_width-font_width*term->cols)/2;
+                offset_height = (win_height-font_height*term->rows)/2;
+                InvalidateRect(wgs.term_hwnd, NULL, true);
+#ifdef RDB_DEBUG_PATCH
+                debug("reset_window() -> Zoomed term_size\n");
+#endif
+            }
+        }
+        return;
+    }
+
+    /* Resize window after DPI change */
+    if (reinit == 3 && p_GetSystemMetricsForDpi && p_AdjustWindowRectExForDpi) {
+        RECT rect;
+        rect.left = rect.top = 0;
+        rect.right = (font_width * term->cols);
+        if (conf_get_bool(conf, CONF_scrollbar))
+            rect.right += p_GetSystemMetricsForDpi(SM_CXVSCROLL,
+                                                   dpi_info.cur_dpi.x);
+        rect.bottom = (font_height * term->rows);
+        p_AdjustWindowRectExForDpi(
+            &rect, GetWindowLongPtr(wgs.term_hwnd, GWL_STYLE),
+            FALSE, GetWindowLongPtr(wgs.term_hwnd, GWL_EXSTYLE),
+            dpi_info.cur_dpi.x);
+        rect.right += (window_border * 2);
+        rect.bottom += (window_border * 2);
+        OffsetRect(&dpi_info.new_wnd_rect,
+                   ((dpi_info.new_wnd_rect.right - dpi_info.new_wnd_rect.left) -
+                    (rect.right - rect.left)) / 2,
+                   ((dpi_info.new_wnd_rect.bottom - dpi_info.new_wnd_rect.top) -
+                    (rect.bottom - rect.top)) / 2);
+        SetWindowPos(wgs.term_hwnd, NULL,
+                     dpi_info.new_wnd_rect.left, dpi_info.new_wnd_rect.top,
+                     rect.right - rect.left, rect.bottom - rect.top,
+                     SWP_NOZORDER);
+
+        InvalidateRect(wgs.term_hwnd, NULL, true);
+        return;
+    }
+
+    /* Hmm, a force re-init means we should ignore the current window
+     * so we resize to the default font size.
+     */
+    if (reinit>0) {
+#ifdef RDB_DEBUG_PATCH
+        debug("reset_window() -> Forced re-init\n");
+#endif
+
+        offset_width = offset_height = window_border;
+        extra_width = wr.right - wr.left - cr.right + cr.left + offset_width*2;
+        extra_height = wr.bottom - wr.top - cr.bottom + cr.top +offset_height*2;
+
+        if (win_width != font_width*term->cols + offset_width*2 ||
+            win_height != font_height*term->rows + offset_height*2) {
+
+            /* If this is too large windows will resize it to the maximum
+             * allowed window size, we will then be back in here and resize
+             * the font or terminal to fit.
+             */
+            SetWindowPos(wgs.term_hwnd, NULL, 0, 0,
+                         font_width*term->cols + extra_width,
+                         font_height*term->rows + extra_height,
+                         SWP_NOMOVE | SWP_NOZORDER);
+        }
+
+        InvalidateRect(wgs.term_hwnd, NULL, true);
+        return;
+    }
+
+    /* Okay the user doesn't want us to change the font so we try the
+     * window. But that may be too big for the screen which forces us
+     * to change the terminal.
+     */
+    if ((resize_action == RESIZE_TERM && reinit<=0) ||
+        (resize_action == RESIZE_EITHER && reinit<0) ||
+        reinit>0) {
+        offset_width = offset_height = window_border;
+        extra_width = wr.right - wr.left - cr.right + cr.left + offset_width*2;
+        extra_height = wr.bottom - wr.top - cr.bottom + cr.top +offset_height*2;
+
+        if (win_width != font_width*term->cols + offset_width*2 ||
+            win_height != font_height*term->rows + offset_height*2) {
+
+            static RECT ss;
+            int width, height;
+
+            get_fullscreen_rect(&ss);
+
+            width = (ss.right - ss.left - extra_width) / font_width;
+            height = (ss.bottom - ss.top - extra_height) / font_height;
+
+            /* Grrr too big */
+            if ( term->rows > height || term->cols > width ) {
+                if (resize_action == RESIZE_EITHER) {
+                    /* Make the font the biggest we can */
+                    if (term->cols > width)
+                        font_width = (ss.right - ss.left - extra_width)
+                            / term->cols;
+                    if (term->rows > height)
+                        font_height = (ss.bottom - ss.top - extra_height)
+                            / term->rows;
+
+                    deinit_fonts();
+                    init_fonts(font_width, font_height);
+
+                    width = (ss.right - ss.left - extra_width) / font_width;
+                    height = (ss.bottom - ss.top - extra_height) / font_height;
+                } else {
+                    if ( height > term->rows ) height = term->rows;
+                    if ( width > term->cols )  width = term->cols;
+                    term_size(term, height, width,
+                              conf_get_int(conf, CONF_savelines));
+#ifdef RDB_DEBUG_PATCH
+                    debug("reset_window() -> term resize to (%d,%d)\n",
+                          height, width);
+#endif
+                }
+            }
+
+            SetWindowPos(wgs.term_hwnd, NULL, 0, 0,
+                         font_width*term->cols + extra_width,
+                         font_height*term->rows + extra_height,
+                         SWP_NOMOVE | SWP_NOZORDER);
+
+            InvalidateRect(wgs.term_hwnd, NULL, true);
+#ifdef RDB_DEBUG_PATCH
+            debug("reset_window() -> window resize to (%d,%d)\n",
+                  font_width*term->cols + extra_width,
+                  font_height*term->rows + extra_height);
+#endif
+        }
+        return;
+    }
+
+    /* We're allowed to or must change the font but do we want to ?  */
+
+    if (font_width != (win_width-window_border*2)/term->cols ||
+        font_height != (win_height-window_border*2)/term->rows) {
+
+        deinit_fonts();
+        init_fonts((win_width-window_border*2)/term->cols,
+                   (win_height-window_border*2)/term->rows);
+        offset_width = (win_width-font_width*term->cols)/2;
+        offset_height = (win_height-font_height*term->rows)/2;
+
+        extra_width = wr.right - wr.left - cr.right + cr.left +offset_width*2;
+        extra_height = wr.bottom - wr.top - cr.bottom + cr.top+offset_height*2;
+
+        InvalidateRect(wgs.term_hwnd, NULL, true);
+#ifdef RDB_DEBUG_PATCH
+        debug("reset_window() -> font resize to (%d,%d)\n",
+              font_width, font_height);
+#endif
+    }
+}
+
+static void set_input_locale(HKL kl)
+{
+    char lbuf[20];
+
+    GetLocaleInfo(LOWORD(kl), LOCALE_IDEFAULTANSICODEPAGE,
+                  lbuf, sizeof(lbuf));
+
+    kbd_codepage = atoi(lbuf);
+}
+
+static void click(Mouse_Button b, int x, int y,
+                  bool shift, bool ctrl, bool alt)
+{
+    int thistime = GetMessageTime();
+
+    if (send_raw_mouse &&
+        !(shift && conf_get_bool(conf, CONF_mouse_override))) {
+        lastbtn = MBT_NOTHING;
+        term_mouse(term, b, translate_button(b), MA_CLICK,
+                   x, y, shift, ctrl, alt);
+        return;
+    }
+
+    if (lastbtn == b && thistime - lasttime < dbltime) {
+        lastact = (lastact == MA_CLICK ? MA_2CLK :
+                   lastact == MA_2CLK ? MA_3CLK :
+                   lastact == MA_3CLK ? MA_CLICK : MA_NOTHING);
+    } else {
+        lastbtn = b;
+        lastact = MA_CLICK;
+    }
+    if (lastact != MA_NOTHING)
+        term_mouse(term, b, translate_button(b), lastact,
+                   x, y, shift, ctrl, alt);
+    lasttime = thistime;
+}
+
+/*
+ * Translate a raw mouse button designation (LEFT, MIDDLE, RIGHT)
+ * into a cooked one (SELECT, EXTEND, PASTE).
+ */
+static Mouse_Button translate_button(Mouse_Button button)
+{
+    if (button == MBT_LEFT)
+        return MBT_SELECT;
+    if (button == MBT_MIDDLE)
+        return conf_get_int(conf, CONF_mouse_is_xterm) == 1 ?
+            MBT_PASTE : MBT_EXTEND;
+    if (button == MBT_RIGHT)
+        return conf_get_int(conf, CONF_mouse_is_xterm) == 1 ?
+            MBT_EXTEND : MBT_PASTE;
+    return 0;                          /* shouldn't happen */
+}
+
+static void show_mouseptr(bool show)
+{
+    /* NB that the counter in ShowCursor() is also frobbed by
+     * update_mouse_pointer() */
+    static bool cursor_visible = true;
+    if (!conf_get_bool(conf, CONF_hide_mouseptr))
+        show = true;                   /* override if this feature disabled */
+    if (cursor_visible && !show)
+        ShowCursor(false);
+    else if (!cursor_visible && show)
+        ShowCursor(true);
+    cursor_visible = show;
+}
+
+static bool is_alt_pressed(void)
+{
+    BYTE keystate[256];
+    int r = GetKeyboardState(keystate);
+    if (!r)
+        return false;
+    if (keystate[VK_MENU] & 0x80)
+        return true;
+    if (keystate[VK_RMENU] & 0x80)
+        return true;
+    return false;
+}
+
+static bool resizing;
+
+static void exit_callback(void *vctx)
+{
+    int exitcode, close_on_exit;
+
+    if (!session_closed &&
+        (exitcode = backend_exitcode(backend)) >= 0) {
+        close_on_exit = conf_get_int(conf, CONF_close_on_exit);
+        /* Abnormal exits will already have set session_closed and taken
+         * appropriate action. */
+        if (close_on_exit == FORCE_ON ||
+            (close_on_exit == AUTO && exitcode != INT_MAX)) {
+            PostQuitMessage(0);
+        } else {
+            queue_toplevel_callback(close_session, NULL);
+            session_closed = true;
+            /* exitcode == INT_MAX indicates that the connection was closed
+             * by a fatal error, so an error box will be coming our way and
+             * we should not generate this informational one. */
+            if (exitcode != INT_MAX) {
+                show_mouseptr(true);
+                MessageBox(wgs.term_hwnd, "Connection closed by remote host",
+                           appname, MB_OK | MB_ICONINFORMATION);
+            }
+        }
+    }
+}
+
+static void win_seat_notify_remote_exit(Seat *seat)
+{
+    queue_toplevel_callback(exit_callback, NULL);
+}
+
+void timer_change_notify(unsigned long next)
+{
+    unsigned long now = GETTICKCOUNT();
+    long ticks;
+    if (now - next < INT_MAX)
+        ticks = 0;
+    else
+        ticks = next - now;
+    KillTimer(wgs.term_hwnd, TIMING_TIMER_ID);
+    SetTimer(wgs.term_hwnd, TIMING_TIMER_ID, ticks, NULL);
+    timing_next_time = next;
+}
+
+static void conf_cache_data(void)
+{
+    /* Cache some items from conf to speed lookups in very hot code */
+    cursor_type = conf_get_int(conf, CONF_cursor_type);
+    vtmode = conf_get_int(conf, CONF_vtmode);
+}
+
+static const int clips_system[] = { CLIP_SYSTEM };
+
+static HDC make_hdc(void)
+{
+    HDC hdc;
+
+    if (!wgs.term_hwnd)
+        return NULL;
+
+    hdc = GetDC(wgs.term_hwnd);
+    if (!hdc)
+        return NULL;
+
+    SelectPalette(hdc, pal, false);
+    return hdc;
+}
+
+static void free_hdc(HDC hdc)
+{
+    assert(wgs.term_hwnd);
+    SelectPalette(hdc, GetStockObject(DEFAULT_PALETTE), false);
+    ReleaseDC(wgs.term_hwnd, hdc);
+}
+
+static bool need_backend_resize = false;
+
+static void wm_size_resize_term(LPARAM lParam, bool border)
+{
+    int width = LOWORD(lParam);
+    int height = HIWORD(lParam);
+    int border_size = border ? conf_get_int(conf, CONF_window_border) : 0;
+
+    int w = (width - border_size*2) / font_width;
+    int h = (height - border_size*2) / font_height;
+
+    if (w < 1) w = 1;
+    if (h < 1) h = 1;
+
+    if (resizing) {
+        /*
+         * If we're in the middle of an interactive resize, we don't
+         * call term_size. This means that, firstly, the user can drag
+         * the size back and forth indecisively without wiping out any
+         * actual terminal contents, and secondly, the Terminal
+         * doesn't call back->size in turn for each increment of the
+         * resizing drag, so we don't spam the server with huge
+         * numbers of resize events.
+         */
+        need_backend_resize = true;
+        conf_set_int(conf, CONF_height, h);
+        conf_set_int(conf, CONF_width, w);
+    } else {
+        term_size(term, h, w,
+                  conf_get_int(conf, CONF_savelines));
+    }
+}
+
+static LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
+                                WPARAM wParam, LPARAM lParam)
+{
+    HDC hdc;
+    static bool ignore_clip = false;
+    static bool fullscr_on_max = false;
+    static bool processed_resize = false;
+    static bool in_scrollbar_loop = false;
+    static UINT last_mousemove = 0;
+    int resize_action;
+
+    switch (message) {
+      case WM_TIMER:
+        if ((UINT_PTR)wParam == TIMING_TIMER_ID) {
+            unsigned long next;
+
+            KillTimer(hwnd, TIMING_TIMER_ID);
+            if (run_timers(timing_next_time, &next)) {
+                timer_change_notify(next);
+            } else {
+            }
+        }
+        return 0;
+      case WM_CREATE:
+        break;
+      case WM_CLOSE: {
+        char *title, *msg, *additional = NULL;
+        show_mouseptr(true);
+        title = dupprintf("%s Exit Confirmation", appname);
+        if (backend && backend->vt->close_warn_text) {
+            additional = backend->vt->close_warn_text(backend);
+        }
+        msg = dupprintf("Are you sure you want to close this session?%s%s",
+                        additional ? "\n" : "",
+                        additional ? additional : "");
+        if (session_closed || !conf_get_bool(conf, CONF_warn_on_close) ||
+            MessageBox(hwnd, msg, title,
+                       MB_ICONWARNING | MB_OKCANCEL | MB_DEFBUTTON1)
+            == IDOK)
+            DestroyWindow(hwnd);
+        sfree(title);
+        sfree(msg);
+        sfree(additional);
+        return 0;
+      }
+      case WM_DESTROY:
+        show_mouseptr(true);
+        PostQuitMessage(0);
+        return 0;
+      case WM_INITMENUPOPUP:
+        if ((HMENU)wParam == savedsess_menu) {
+            /* About to pop up Saved Sessions sub-menu.
+             * Refresh the session list. */
+            get_sesslist(&sesslist, false); /* free */
+            get_sesslist(&sesslist, true);
+            update_savedsess_menu();
+            return 0;
+        }
+        break;
+      case WM_COMMAND:
+      case WM_SYSCOMMAND:
+        switch (wParam & ~0xF) {       /* low 4 bits reserved to Windows */
+          case SC_VSCROLL:
+          case SC_HSCROLL:
+            if (message == WM_SYSCOMMAND) {
+                /* As per the long comment in WM_VSCROLL handler: give
+                 * this message the default handling, which starts a
+                 * subsidiary message loop, but set a flag so that
+                 * when we're re-entered from that loop, scroll events
+                 * within an interactive scrollbar-drag can be handled
+                 * differently. */
+                in_scrollbar_loop = true;
+                LRESULT result = sw_DefWindowProc(
+                    hwnd, message, wParam, lParam);
+                in_scrollbar_loop = false;
+                return result;
+            }
+            break;
+          case IDM_SHOWLOG:
+            showeventlog(hwnd);
+            break;
+          case IDM_NEWSESS:
+          case IDM_DUPSESS:
+          case IDM_SAVEDSESS: {
+            char b[2048];
+            char *cl;
+            const char *argprefix;
+            bool inherit_handles;
+            STARTUPINFO si;
+            PROCESS_INFORMATION pi;
+            HANDLE filemap = NULL;
+
+            if (restricted_acl())
+                argprefix = "&R";
+            else
+                argprefix = "";
+
+            if (wParam == IDM_DUPSESS) {
+                /*
+                 * Allocate a file-mapping memory chunk for the
+                 * config structure.
+                 */
+                SECURITY_ATTRIBUTES sa;
+                strbuf *serbuf;
+                void *p;
+                int size;
+
+                serbuf = strbuf_new();
+                conf_serialise(BinarySink_UPCAST(serbuf), conf);
+                size = serbuf->len;
+
+                sa.nLength = sizeof(sa);
+                sa.lpSecurityDescriptor = NULL;
+                sa.bInheritHandle = true;
+                filemap = CreateFileMapping(INVALID_HANDLE_VALUE,
+                                            &sa,
+                                            PAGE_READWRITE,
+                                            0, size, NULL);
+                if (filemap && filemap != INVALID_HANDLE_VALUE) {
+                    p = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, size);
+                    if (p) {
+                        memcpy(p, serbuf->s, size);
+                        UnmapViewOfFile(p);
+                    }
+                }
+
+                strbuf_free(serbuf);
+                inherit_handles = true;
+                cl = dupprintf("putty %s&%p:%u", argprefix,
+                               filemap, (unsigned)size);
+            } else if (wParam == IDM_SAVEDSESS) {
+                unsigned int sessno = ((lParam - IDM_SAVED_MIN)
+                                       / MENU_SAVED_STEP) + 1;
+                if (sessno < (unsigned)sesslist.nsessions) {
+                    const char *session = sesslist.sessions[sessno];
+                    cl = dupprintf("putty %s@%s", argprefix, session);
+                    inherit_handles = false;
+                } else
+                    break;
+            } else /* IDM_NEWSESS */ {
+                cl = dupprintf("putty%s%s",
+                               *argprefix ? " " : "",
+                               argprefix);
+                inherit_handles = false;
+            }
+
+            GetModuleFileName(NULL, b, sizeof(b) - 1);
+            si.cb = sizeof(si);
+            si.lpReserved = NULL;
+            si.lpDesktop = NULL;
+            si.lpTitle = NULL;
+            si.dwFlags = 0;
+            si.cbReserved2 = 0;
+            si.lpReserved2 = NULL;
+            CreateProcess(b, cl, NULL, NULL, inherit_handles,
+                          NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi);
+            CloseHandle(pi.hProcess);
+            CloseHandle(pi.hThread);
+
+            if (filemap)
+                CloseHandle(filemap);
+            sfree(cl);
+            break;
+          }
+          case IDM_RESTART:
+            if (!backend) {
+                lp_eventlog(&wgs.logpolicy, "----- Session restarted -----");
+                term_pwron(term, false);
+                start_backend();
+            }
+
+            break;
+          case IDM_RECONF: {
+            Conf *prev_conf;
+            int init_lvl = 1;
+            bool reconfig_result;
+
+            if (reconfiguring)
+                break;
+            else
+                reconfiguring = true;
+
+            term_pre_reconfig(term, conf);
+            prev_conf = conf_copy(conf);
+
+            reconfig_result = do_reconfig(
+                hwnd, conf, backend ? backend_cfg_info(backend) : 0);
+            reconfiguring = false;
+            if (!reconfig_result) {
+                conf_free(prev_conf);
+                break;
+            }
+
+            conf_cache_data();
+
+            resize_action = conf_get_int(conf, CONF_resize_action);
+            {
+                /* Disable full-screen if resizing forbidden */
+                int i;
+                for (i = 0; i < lenof(popup_menus); i++)
+                    EnableMenuItem(popup_menus[i].menu, IDM_FULLSCREEN,
+                                   MF_BYCOMMAND |
+                                   (resize_action == RESIZE_DISABLED
+                                    ? MF_GRAYED : MF_ENABLED));
+                /* Gracefully unzoom if necessary */
+                if (IsZoomed(hwnd) && (resize_action == RESIZE_DISABLED))
+                    ShowWindow(hwnd, SW_RESTORE);
+            }
+
+            /* Pass new config data to the logging module */
+            log_reconfig(logctx, conf);
+
+            sfree(logpal);
+            /*
+             * Flush the line discipline's edit buffer in the
+             * case where local editing has just been disabled.
+             */
+            if (ldisc) {
+                ldisc_configure(ldisc, conf);
+                ldisc_echoedit_update(ldisc);
+            }
+
+            if (conf_get_bool(conf, CONF_system_colour) !=
+                conf_get_bool(prev_conf, CONF_system_colour))
+                term_notify_palette_changed(term);
+
+            /* Pass new config data to the terminal */
+            term_reconfig(term, conf);
+            setup_clipboards(term, conf);
+
+            /* Reinitialise the colour palette, in case the terminal
+             * just read new settings out of Conf */
+            if (pal)
+                DeleteObject(pal);
+            logpal = NULL;
+            pal = NULL;
+            init_palette();
+
+            /* Pass new config data to the back end */
+            if (backend)
+                backend_reconfig(backend, conf);
+
+            /* Screen size changed ? */
+            if (conf_get_int(conf, CONF_height) !=
+                conf_get_int(prev_conf, CONF_height) ||
+                conf_get_int(conf, CONF_width) !=
+                conf_get_int(prev_conf, CONF_width) ||
+                conf_get_int(conf, CONF_savelines) !=
+                conf_get_int(prev_conf, CONF_savelines) ||
+                resize_action == RESIZE_FONT ||
+                (resize_action == RESIZE_EITHER && IsZoomed(hwnd)) ||
+                resize_action == RESIZE_DISABLED)
+                term_size(term, conf_get_int(conf, CONF_height),
+                          conf_get_int(conf, CONF_width),
+                          conf_get_int(conf, CONF_savelines));
+
+            /* Enable or disable the scroll bar, etc */
+            {
+                LONG nflg, flag = GetWindowLongPtr(hwnd, GWL_STYLE);
+                LONG nexflag, exflag =
+                    GetWindowLongPtr(hwnd, GWL_EXSTYLE);
+
+                nexflag = exflag;
+                if (conf_get_bool(conf, CONF_alwaysontop) !=
+                    conf_get_bool(prev_conf, CONF_alwaysontop)) {
+                    if (conf_get_bool(conf, CONF_alwaysontop)) {
+                        nexflag |= WS_EX_TOPMOST;
+                        SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
+                                     SWP_NOMOVE | SWP_NOSIZE);
+                    } else {
+                        nexflag &= ~(WS_EX_TOPMOST);
+                        SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0,
+                                     SWP_NOMOVE | SWP_NOSIZE);
+                    }
+                }
+                if (conf_get_bool(conf, CONF_sunken_edge))
+                    nexflag |= WS_EX_CLIENTEDGE;
+                else
+                    nexflag &= ~(WS_EX_CLIENTEDGE);
+
+                nflg = flag;
+                if (conf_get_bool(conf, is_full_screen() ?
+                                  CONF_scrollbar_in_fullscreen :
+                                  CONF_scrollbar))
+                    nflg |= WS_VSCROLL;
+                else
+                    nflg &= ~WS_VSCROLL;
+
+                if (resize_action == RESIZE_DISABLED ||
+                    is_full_screen())
+                    nflg &= ~WS_THICKFRAME;
+                else
+                    nflg |= WS_THICKFRAME;
+
+                if (resize_action == RESIZE_DISABLED)
+                    nflg &= ~WS_MAXIMIZEBOX;
+                else
+                    nflg |= WS_MAXIMIZEBOX;
+
+                if (nflg != flag || nexflag != exflag) {
+                    if (nflg != flag)
+                        SetWindowLongPtr(hwnd, GWL_STYLE, nflg);
+                    if (nexflag != exflag)
+                        SetWindowLongPtr(hwnd, GWL_EXSTYLE, nexflag);
+
+                    SetWindowPos(hwnd, NULL, 0, 0, 0, 0,
+                                 SWP_NOACTIVATE | SWP_NOCOPYBITS |
+                                 SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
+                                 SWP_FRAMECHANGED);
+
+                    init_lvl = 2;
+                }
+            }
+
+            /* Oops */
+            if (resize_action == RESIZE_DISABLED && IsZoomed(hwnd)) {
+                force_normal(hwnd);
+                init_lvl = 2;
+            }
+
+            {
+                FontSpec *font = conf_get_fontspec(conf, CONF_font);
+                FontSpec *prev_font = conf_get_fontspec(prev_conf,
+                                                        CONF_font);
+
+                if (!strcmp(font->name, prev_font->name) ||
+                    !strcmp(conf_get_str(conf, CONF_line_codepage),
+                            conf_get_str(prev_conf, CONF_line_codepage)) ||
+                    font->isbold != prev_font->isbold ||
+                    font->height != prev_font->height ||
+                    font->charset != prev_font->charset ||
+                    conf_get_int(conf, CONF_font_quality) !=
+                    conf_get_int(prev_conf, CONF_font_quality) ||
+                    conf_get_int(conf, CONF_vtmode) !=
+                    conf_get_int(prev_conf, CONF_vtmode) ||
+                    conf_get_int(conf, CONF_bold_style) !=
+                    conf_get_int(prev_conf, CONF_bold_style) ||
+                    resize_action == RESIZE_DISABLED ||
+                    resize_action == RESIZE_EITHER ||
+                    resize_action != conf_get_int(prev_conf,
+                                                  CONF_resize_action))
+                    init_lvl = 2;
+            }
+
+            InvalidateRect(hwnd, NULL, true);
+            reset_window(init_lvl);
+
+            conf_free(prev_conf);
+            break;
+          }
+          case IDM_COPYALL:
+            term_copyall(term, clips_system, lenof(clips_system));
+            break;
+          case IDM_COPY:
+            term_request_copy(term, clips_system, lenof(clips_system));
+            break;
+          case IDM_PASTE:
+            term_request_paste(term, CLIP_SYSTEM);
+            break;
+          case IDM_CLRSB:
+            term_clrsb(term);
+            break;
+          case IDM_RESET:
+            term_pwron(term, true);
+            if (ldisc)
+                ldisc_echoedit_update(ldisc);
+            break;
+          case IDM_ABOUT:
+            showabout(hwnd);
+            break;
+          case IDM_HELP:
+            launch_help(hwnd, NULL);
+            break;
+          case SC_MOUSEMENU:
+            /*
+             * We get this if the System menu has been activated
+             * using the mouse.
+             */
+            show_mouseptr(true);
+            break;
+          case SC_KEYMENU:
+            /*
+             * We get this if the System menu has been activated
+             * using the keyboard. This might happen from within
+             * TranslateKey, in which case it really wants to be
+             * followed by a `space' character to actually _bring
+             * the menu up_ rather than just sitting there in
+             * `ready to appear' state.
+             */
+            show_mouseptr(true);    /* make sure pointer is visible */
+            if( lParam == 0 )
+                PostMessage(hwnd, WM_CHAR, ' ', 0);
+            break;
+          case IDM_FULLSCREEN:
+            flip_full_screen();
+            break;
+          default:
+            if (wParam >= IDM_SAVED_MIN && wParam < IDM_SAVED_MAX) {
+                SendMessage(hwnd, WM_SYSCOMMAND, IDM_SAVEDSESS, wParam);
+            }
+            if (wParam >= IDM_SPECIAL_MIN && wParam <= IDM_SPECIAL_MAX) {
+                int i = (wParam - IDM_SPECIAL_MIN) / 0x10;
+                /*
+                 * Ensure we haven't been sent a bogus SYSCOMMAND
+                 * which would cause us to reference invalid memory
+                 * and crash. Perhaps I'm just too paranoid here.
+                 */
+                if (i >= n_specials)
+                    break;
+                if (backend)
+                    backend_special(
+                        backend, specials[i].code, specials[i].arg);
+            }
+        }
+        break;
+
+#define X_POS(l) ((int)(short)LOWORD(l))
+#define Y_POS(l) ((int)(short)HIWORD(l))
+
+#define TO_CHR_X(x) ((((x)<0 ? (x)-font_width+1 : (x))-offset_width) / font_width)
+#define TO_CHR_Y(y) ((((y)<0 ? (y)-font_height+1: (y))-offset_height) / font_height)
+      case WM_LBUTTONDOWN:
+      case WM_MBUTTONDOWN:
+      case WM_RBUTTONDOWN:
+      case WM_LBUTTONUP:
+      case WM_MBUTTONUP:
+      case WM_RBUTTONUP:
+        if (message == WM_RBUTTONDOWN &&
+            ((wParam & MK_CONTROL) ||
+             (conf_get_int(conf, CONF_mouse_is_xterm) == 2))) {
+            POINT cursorpos;
+
+            /* Just in case this happened in mid-select */
+            term_cancel_selection_drag(term);
+
+            show_mouseptr(true);    /* make sure pointer is visible */
+            GetCursorPos(&cursorpos);
+            TrackPopupMenu(popup_menus[CTXMENU].menu,
+                           TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RIGHTBUTTON,
+                           cursorpos.x, cursorpos.y,
+                           0, hwnd, NULL);
+            break;
+        }
+        {
+            int button;
+            bool press;
+
+            switch (message) {
+              case WM_LBUTTONDOWN:
+                button = MBT_LEFT;
+                wParam |= MK_LBUTTON;
+                press = true;
+                break;
+              case WM_MBUTTONDOWN:
+                button = MBT_MIDDLE;
+                wParam |= MK_MBUTTON;
+                press = true;
+                break;
+              case WM_RBUTTONDOWN:
+                button = MBT_RIGHT;
+                wParam |= MK_RBUTTON;
+                press = true;
+                break;
+              case WM_LBUTTONUP:
+                button = MBT_LEFT;
+                wParam &= ~MK_LBUTTON;
+                press = false;
+                break;
+              case WM_MBUTTONUP:
+                button = MBT_MIDDLE;
+                wParam &= ~MK_MBUTTON;
+                press = false;
+                break;
+              case WM_RBUTTONUP:
+                button = MBT_RIGHT;
+                wParam &= ~MK_RBUTTON;
+                press = false;
+                break;
+              default: /* shouldn't happen */
+                button = 0;
+                press = false;
+            }
+            show_mouseptr(true);
+            /*
+             * Special case: in full-screen mode, if the left
+             * button is clicked in the very top left corner of the
+             * window, we put up the System menu instead of doing
+             * selection.
+             */
+            {
+                bool mouse_on_hotspot = false;
+                POINT pt;
+
+                GetCursorPos(&pt);
+#ifndef NO_MULTIMON
+                if (p_GetMonitorInfoA && p_MonitorFromPoint) {
+                    HMONITOR mon;
+                    MONITORINFO mi;
+
+                    mon = p_MonitorFromPoint(pt, MONITOR_DEFAULTTONULL);
+
+                    if (mon != NULL) {
+                        mi.cbSize = sizeof(MONITORINFO);
+                        p_GetMonitorInfoA(mon, &mi);
+
+                        if (mi.rcMonitor.left == pt.x &&
+                            mi.rcMonitor.top == pt.y) {
+                            mouse_on_hotspot = true;
+                        }
+                    }
+                } else
+#endif
+                if (pt.x == 0 && pt.y == 0) {
+                    mouse_on_hotspot = true;
+                }
+                if (is_full_screen() && press &&
+                    button == MBT_LEFT && mouse_on_hotspot) {
+                    SendMessage(hwnd, WM_SYSCOMMAND, SC_MOUSEMENU,
+                                MAKELPARAM(pt.x, pt.y));
+                    return 0;
+                }
+            }
+
+            if (press) {
+                click(button,
+                      TO_CHR_X(X_POS(lParam)), TO_CHR_Y(Y_POS(lParam)),
+                      wParam & MK_SHIFT, wParam & MK_CONTROL,
+                      is_alt_pressed());
+                SetCapture(hwnd);
+            } else {
+                term_mouse(term, button, translate_button(button), MA_RELEASE,
+                           TO_CHR_X(X_POS(lParam)),
+                           TO_CHR_Y(Y_POS(lParam)), wParam & MK_SHIFT,
+                           wParam & MK_CONTROL, is_alt_pressed());
+                if (!(wParam & (MK_LBUTTON | MK_MBUTTON | MK_RBUTTON)))
+                    ReleaseCapture();
+            }
+        }
+        return 0;
+      case WM_MOUSEMOVE: {
+        /*
+         * Windows seems to like to occasionally send MOUSEMOVE
+         * events even if the mouse hasn't moved. Don't unhide
+         * the mouse pointer in this case.
+         */
+        static WPARAM wp = 0;
+        static LPARAM lp = 0;
+        if (wParam != wp || lParam != lp ||
+            last_mousemove != WM_MOUSEMOVE) {
+            show_mouseptr(true);
+            wp = wParam; lp = lParam;
+            last_mousemove = WM_MOUSEMOVE;
+        }
+        /*
+         * Add the mouse position and message time to the random
+         * number noise.
+         */
+        noise_ultralight(NOISE_SOURCE_MOUSEPOS, lParam);
+
+        if (wParam & (MK_LBUTTON | MK_MBUTTON | MK_RBUTTON) &&
+            GetCapture() == hwnd) {
+            Mouse_Button b;
+            if (wParam & MK_LBUTTON)
+                b = MBT_LEFT;
+            else if (wParam & MK_MBUTTON)
+                b = MBT_MIDDLE;
+            else
+                b = MBT_RIGHT;
+            term_mouse(term, b, translate_button(b), MA_DRAG,
+                       TO_CHR_X(X_POS(lParam)),
+                       TO_CHR_Y(Y_POS(lParam)), wParam & MK_SHIFT,
+                       wParam & MK_CONTROL, is_alt_pressed());
+        } else {
+            term_mouse(term, MBT_NOTHING, MBT_NOTHING, MA_MOVE,
+                       TO_CHR_X(X_POS(lParam)),
+                       TO_CHR_Y(Y_POS(lParam)), false,
+                       false, false);
+        }
+        return 0;
+      }
+      case WM_NCMOUSEMOVE: {
+        static WPARAM wp = 0;
+        static LPARAM lp = 0;
+        if (wParam != wp || lParam != lp ||
+            last_mousemove != WM_NCMOUSEMOVE) {
+            show_mouseptr(true);
+            wp = wParam; lp = lParam;
+            last_mousemove = WM_NCMOUSEMOVE;
+        }
+        noise_ultralight(NOISE_SOURCE_MOUSEPOS, lParam);
+        break;
+      }
+      case WM_IGNORE_CLIP:
+        ignore_clip = wParam;          /* don't panic on DESTROYCLIPBOARD */
+        break;
+      case WM_DESTROYCLIPBOARD:
+        if (!ignore_clip)
+            term_lost_clipboard_ownership(term, CLIP_SYSTEM);
+        ignore_clip = false;
+        return 0;
+      case WM_PAINT: {
+        PAINTSTRUCT p;
+
+        HideCaret(hwnd);
+        hdc = BeginPaint(hwnd, &p);
+        if (pal) {
+            SelectPalette(hdc, pal, true);
+            RealizePalette(hdc);
+        }
+
+        /*
+         * We have to be careful about term_paint(). It will
+         * set a bunch of character cells to INVALID and then
+         * call do_paint(), which will redraw those cells and
+         * _then mark them as done_. This may not be accurate:
+         * when painting in WM_PAINT context we are restricted
+         * to the rectangle which has just been exposed - so if
+         * that only covers _part_ of a character cell and the
+         * rest of it was already visible, that remainder will
+         * not be redrawn at all. Accordingly, we must not
+         * paint any character cell in a WM_PAINT context which
+         * already has a pending update due to terminal output.
+         * The simplest solution to this - and many, many
+         * thanks to Hung-Te Lin for working all this out - is
+         * not to do any actual painting at _all_ if there's a
+         * pending terminal update: just mark the relevant
+         * character cells as INVALID and wait for the
+         * scheduled full update to sort it out.
+         *
+         * I have a suspicion this isn't the _right_ solution.
+         * An alternative approach would be to have terminal.c
+         * separately track what _should_ be on the terminal
+         * screen and what _is_ on the terminal screen, and
+         * have two completely different types of redraw (one
+         * for full updates, which syncs the former with the
+         * terminal itself, and one for WM_PAINT which syncs
+         * the latter with the former); yet another possibility
+         * would be to have the Windows front end do what the
+         * GTK one already does, and maintain a bitmap of the
+         * current terminal appearance so that WM_PAINT becomes
+         * completely trivial. However, this should do for now.
+         */
+        assert(!wintw_hdc);
+        wintw_hdc = hdc;
+        term_paint(term,
+                   (p.rcPaint.left-offset_width)/font_width,
+                   (p.rcPaint.top-offset_height)/font_height,
+                   (p.rcPaint.right-offset_width-1)/font_width,
+                   (p.rcPaint.bottom-offset_height-1)/font_height,
+                   !term->window_update_pending);
+        wintw_hdc = NULL;
+
+        if (p.fErase ||
+            p.rcPaint.left  < offset_width  ||
+            p.rcPaint.top   < offset_height ||
+            p.rcPaint.right >= offset_width + font_width*term->cols ||
+            p.rcPaint.bottom>= offset_height + font_height*term->rows)
+        {
+            HBRUSH fillcolour, oldbrush;
+            HPEN   edge, oldpen;
+            fillcolour = CreateSolidBrush (
+                colours[ATTR_DEFBG>>ATTR_BGSHIFT]);
+            oldbrush = SelectObject(hdc, fillcolour);
+            edge = CreatePen(PS_SOLID, 0,
+                             colours[ATTR_DEFBG>>ATTR_BGSHIFT]);
+            oldpen = SelectObject(hdc, edge);
+
+            /*
+             * Jordan Russell reports that this apparently
+             * ineffectual IntersectClipRect() call masks a
+             * Windows NT/2K bug causing strange display
+             * problems when the PuTTY window is taller than
+             * the primary monitor. It seems harmless enough...
+             */
+            IntersectClipRect(hdc,
+                              p.rcPaint.left, p.rcPaint.top,
+                              p.rcPaint.right, p.rcPaint.bottom);
+
+            ExcludeClipRect(hdc,
+                            offset_width, offset_height,
+                            offset_width+font_width*term->cols,
+                            offset_height+font_height*term->rows);
+
+            Rectangle(hdc, p.rcPaint.left, p.rcPaint.top,
+                      p.rcPaint.right, p.rcPaint.bottom);
+
+            /* SelectClipRgn(hdc, NULL); */
+
+            SelectObject(hdc, oldbrush);
+            DeleteObject(fillcolour);
+            SelectObject(hdc, oldpen);
+            DeleteObject(edge);
+        }
+        SelectObject(hdc, GetStockObject(SYSTEM_FONT));
+        SelectObject(hdc, GetStockObject(WHITE_PEN));
+        EndPaint(hwnd, &p);
+        ShowCaret(hwnd);
+        return 0;
+      }
+      case WM_NETEVENT:
+        winselgui_response(wParam, lParam);
+        return 0;
+      case WM_SETFOCUS:
+        term_set_focus(term, true);
+        CreateCaret(hwnd, caretbm, font_width, font_height);
+        ShowCaret(hwnd);
+        flash_window(0);               /* stop */
+        compose_state = 0;
+        term_update(term);
+        break;
+      case WM_KILLFOCUS:
+        show_mouseptr(true);
+        term_set_focus(term, false);
+        DestroyCaret();
+        caret_x = caret_y = -1;        /* ensure caret is replaced next time */
+        term_update(term);
+        break;
+      case WM_ENTERSIZEMOVE:
+#ifdef RDB_DEBUG_PATCH
+        debug("WM_ENTERSIZEMOVE\n");
+#endif
+        EnableSizeTip(true);
+        resizing = true;
+        need_backend_resize = false;
+        break;
+      case WM_EXITSIZEMOVE:
+        EnableSizeTip(false);
+        resizing = false;
+#ifdef RDB_DEBUG_PATCH
+        debug("WM_EXITSIZEMOVE\n");
+#endif
+        if (need_backend_resize) {
+            term_size(term, conf_get_int(conf, CONF_height),
+                      conf_get_int(conf, CONF_width),
+                      conf_get_int(conf, CONF_savelines));
+            InvalidateRect(hwnd, NULL, true);
+        }
+        recompute_window_offset();
+        break;
+      case WM_SIZING:
+        /*
+         * This does two jobs:
+         * 1) Keep the sizetip uptodate
+         * 2) Make sure the window size is _stepped_ in units of the font size.
+         */
+        resize_action = conf_get_int(conf, CONF_resize_action);
+        if (resize_action == RESIZE_TERM ||
+            (resize_action == RESIZE_EITHER && !is_alt_pressed())) {
+            int width, height, w, h, ew, eh;
+            LPRECT r = (LPRECT) lParam;
+
+            if (!need_backend_resize && resize_action == RESIZE_EITHER &&
+                (conf_get_int(conf, CONF_height) != term->rows ||
+                 conf_get_int(conf, CONF_width) != term->cols)) {
+                /*
+                 * Great! It seems that both the terminal size and the
+                 * font size have been changed and the user is now dragging.
+                 *
+                 * It will now be difficult to get back to the configured
+                 * font size!
+                 *
+                 * This would be easier but it seems to be too confusing.
+                 */
+                conf_set_int(conf, CONF_height, term->rows);
+                conf_set_int(conf, CONF_width, term->cols);
+
+                InvalidateRect(hwnd, NULL, true);
+                need_backend_resize = true;
+            }
+
+            width = r->right - r->left - extra_width;
+            height = r->bottom - r->top - extra_height;
+            w = (width + font_width / 2) / font_width;
+            if (w < 1)
+                w = 1;
+            h = (height + font_height / 2) / font_height;
+            if (h < 1)
+                h = 1;
+            UpdateSizeTip(hwnd, w, h);
+            ew = width - w * font_width;
+            eh = height - h * font_height;
+            if (ew != 0) {
+                if (wParam == WMSZ_LEFT ||
+                    wParam == WMSZ_BOTTOMLEFT || wParam == WMSZ_TOPLEFT)
+                    r->left += ew;
+                else
+                    r->right -= ew;
+            }
+            if (eh != 0) {
+                if (wParam == WMSZ_TOP ||
+                    wParam == WMSZ_TOPRIGHT || wParam == WMSZ_TOPLEFT)
+                    r->top += eh;
+                else
+                    r->bottom -= eh;
+            }
+            if (ew || eh)
+                return 1;
+            else
+                return 0;
+        } else {
+            int width, height, w, h, rv = 0;
+            int window_border = conf_get_int(conf, CONF_window_border);
+            int ex_width = extra_width + (window_border - offset_width) * 2;
+            int ex_height = extra_height + (window_border - offset_height) * 2;
+            LPRECT r = (LPRECT) lParam;
+
+            width = r->right - r->left - ex_width;
+            height = r->bottom - r->top - ex_height;
+
+            w = (width + term->cols/2)/term->cols;
+            h = (height + term->rows/2)/term->rows;
+            if ( r->right != r->left + w*term->cols + ex_width)
+                rv = 1;
+
+            if (wParam == WMSZ_LEFT ||
+                wParam == WMSZ_BOTTOMLEFT || wParam == WMSZ_TOPLEFT)
+                r->left = r->right - w*term->cols - ex_width;
+            else
+                r->right = r->left + w*term->cols + ex_width;
+
+            if (r->bottom != r->top + h*term->rows + ex_height)
+                rv = 1;
+
+            if (wParam == WMSZ_TOP ||
+                wParam == WMSZ_TOPRIGHT || wParam == WMSZ_TOPLEFT)
+                r->top = r->bottom - h*term->rows - ex_height;
+            else
+                r->bottom = r->top + h*term->rows + ex_height;
+
+            return rv;
+        }
+        /* break;  (never reached) */
+      case WM_FULLSCR_ON_MAX:
+        fullscr_on_max = true;
+        break;
+      case WM_MOVE:
+        term_notify_window_pos(term, LOWORD(lParam), HIWORD(lParam));
+        sys_cursor_update();
+        break;
+      case WM_SIZE:
+        resize_action = conf_get_int(conf, CONF_resize_action);
+#ifdef RDB_DEBUG_PATCH
+        debug("WM_SIZE %s (%d,%d)\n",
+              (wParam == SIZE_MINIMIZED) ? "SIZE_MINIMIZED":
+              (wParam == SIZE_MAXIMIZED) ? "SIZE_MAXIMIZED":
+              (wParam == SIZE_RESTORED && resizing) ? "to":
+              (wParam == SIZE_RESTORED) ? "SIZE_RESTORED":
+              "...",
+              LOWORD(lParam), HIWORD(lParam));
+#endif
+        term_notify_minimised(term, wParam == SIZE_MINIMIZED);
+        {
+            /*
+             * WM_SIZE's lParam tells us the size of the client area.
+             * But historic PuTTY practice is that we want to tell the
+             * terminal the size of the overall window.
+             */
+            RECT r;
+            GetWindowRect(hwnd, &r);
+            term_notify_window_size_pixels(
+                term, r.right - r.left, r.bottom - r.top);
+        }
+        if (wParam == SIZE_MINIMIZED)
+            sw_SetWindowText(hwnd,
+                             conf_get_bool(conf, CONF_win_name_always) ?
+                             window_name : icon_name);
+        if (wParam == SIZE_RESTORED || wParam == SIZE_MAXIMIZED)
+            sw_SetWindowText(hwnd, window_name);
+        if (wParam == SIZE_RESTORED) {
+            processed_resize = false;
+            clear_full_screen();
+            if (processed_resize) {
+                /*
+                 * Inhibit normal processing of this WM_SIZE; a
+                 * secondary one was triggered just now by
+                 * clear_full_screen which contained the correct
+                 * client area size.
+                 */
+                return 0;
+            }
+        }
+        if (wParam == SIZE_MAXIMIZED && fullscr_on_max) {
+            fullscr_on_max = false;
+            processed_resize = false;
+            make_full_screen();
+            if (processed_resize) {
+                /*
+                 * Inhibit normal processing of this WM_SIZE; a
+                 * secondary one was triggered just now by
+                 * make_full_screen which contained the correct client
+                 * area size.
+                 */
+                return 0;
+            }
+        }
+
+        processed_resize = true;
+
+        if (resize_action == RESIZE_DISABLED) {
+            /* A resize, well it better be a minimize. */
+            reset_window(-1);
+        } else {
+            if (wParam == SIZE_MAXIMIZED) {
+                was_zoomed = true;
+                prev_rows = term->rows;
+                prev_cols = term->cols;
+                if (resize_action == RESIZE_TERM)
+                    wm_size_resize_term(lParam, false);
+                reset_window(0);
+            } else if (wParam == SIZE_RESTORED && was_zoomed) {
+                was_zoomed = false;
+                if (resize_action == RESIZE_TERM) {
+                    wm_size_resize_term(lParam, true);
+                    reset_window(2);
+                } else if (resize_action != RESIZE_FONT)
+                    reset_window(2);
+                else
+                    reset_window(0);
+            } else if (wParam == SIZE_MINIMIZED) {
+                /* do nothing */
+            } else if (resize_action == RESIZE_TERM ||
+                       (resize_action == RESIZE_EITHER &&
+                        !is_alt_pressed())) {
+                wm_size_resize_term(lParam, true);
+
+                /*
+                 * Sometimes, we can get a spontaneous resize event
+                 * outside a WM_SIZING interactive drag which wants to
+                 * set us to a new specific SIZE_RESTORED size. An
+                 * example is what happens if you press Windows+Right
+                 * and then Windows+Up: the first operation fits the
+                 * window to the right-hand half of the screen, and
+                 * the second one changes that for the top right
+                 * quadrant. In that situation, if we've responded
+                 * here by resizing the terminal, we may still need to
+                 * recompute the border around the window and do a
+                 * full redraw to clear the new border.
+                 */
+                if (!resizing)
+                    recompute_window_offset();
+            } else {
+                reset_window(0);
+            }
+        }
+        sys_cursor_update();
+        return 0;
+      case WM_DPICHANGED:
+        dpi_info.cur_dpi.x = LOWORD(wParam);
+        dpi_info.cur_dpi.y = HIWORD(wParam);
+        dpi_info.new_wnd_rect = *(RECT*)(lParam);
+        reset_window(3);
+        return 0;
+      case WM_VSCROLL:
+        switch (LOWORD(wParam)) {
+          case SB_BOTTOM:
+            term_scroll(term, -1, 0);
+            break;
+          case SB_TOP:
+            term_scroll(term, +1, 0);
+            break;
+          case SB_LINEDOWN:
+            term_scroll(term, 0, +1);
+            break;
+          case SB_LINEUP:
+            term_scroll(term, 0, -1);
+            break;
+          case SB_PAGEDOWN:
+            term_scroll(term, 0, +term->rows / 2);
+            break;
+          case SB_PAGEUP:
+            term_scroll(term, 0, -term->rows / 2);
+            break;
+          case SB_THUMBPOSITION:
+          case SB_THUMBTRACK: {
+            /*
+             * Use GetScrollInfo instead of HIWORD(wParam) to get
+             * 32-bit scroll position.
+             */
+            SCROLLINFO si;
+
+            si.cbSize = sizeof(si);
+            si.fMask = SIF_TRACKPOS;
+            if (GetScrollInfo(hwnd, SB_VERT, &si) == 0)
+                si.nTrackPos = HIWORD(wParam);
+            term_scroll(term, 1, si.nTrackPos);
+            break;
+          }
+        }
+
+        if (in_scrollbar_loop) {
+            /*
+             * Allow window updates to happen during interactive
+             * scroll.
+             *
+             * When the user takes hold of our window's scrollbar and
+             * wobbles it interactively back and forth, or presses on
+             * one of the arrow buttons at the ends, the first thing
+             * that happens is that this window procedure receives
+             * WM_SYSCOMMAND / SC_VSCROLL. [1] The default handler for
+             * that window message starts a subsidiary message loop,
+             * which continues to run until the user lets go of the
+             * scrollbar again. All WM_VSCROLL / SB_THUMBTRACK
+             * messages are generated by the handlers within that
+             * subsidiary message loop.
+             *
+             * So, during that time, _our_ message loop is not
+             * running, which means toplevel callbacks and timers and
+             * so forth are not happening, which means that when we
+             * redraw the window and set a timer to clear the cooldown
+             * flag 20ms later, that timer never fires, and we aren't
+             * able to keep redrawing the window.
+             *
+             * The 'obvious' answer would be to seize that SYSCOMMAND
+             * ourselves and inhibit the default handler, so that our
+             * message loop carries on running. But that would mean
+             * we'd have to reimplement the whole of the scrollbar
+             * handler!
+             *
+             * So instead we apply a bodge: set a static variable that
+             * indicates that we're _in_ that sub-loop, and if so,
+             * decide it's OK to manually call term_update() proper,
+             * bypassing the timer and cooldown and rate-limiting
+             * systems completely, whenever we see an SB_THUMBTRACK.
+             * This shouldn't cause a rate overload, because we're
+             * only doing it once per UI event!
+             *
+             * [1] Actually, there's an extra oddity where SC_HSCROLL
+             * and SC_VSCROLL have their documented values the wrong
+             * way round. Many people on the Internet have noticed
+             * this, e.g. https://stackoverflow.com/q/55528397
+             */
+            term_update(term);
+        }
+        break;
+      case WM_PALETTECHANGED:
+        if ((HWND) wParam != hwnd && pal != NULL) {
+            HDC hdc = make_hdc();
+            if (hdc) {
+                if (RealizePalette(hdc) > 0)
+                    UpdateColors(hdc);
+                free_hdc(hdc);
+            }
+        }
+        break;
+      case WM_QUERYNEWPALETTE:
+        if (pal != NULL) {
+            HDC hdc = make_hdc();
+            if (hdc) {
+                if (RealizePalette(hdc) > 0)
+                    UpdateColors(hdc);
+                free_hdc(hdc);
+                return true;
+            }
+        }
+        return false;
+      case WM_KEYDOWN:
+      case WM_SYSKEYDOWN:
+      case WM_KEYUP:
+      case WM_SYSKEYUP:
+        /*
+         * Add the scan code and keypress timing to the random
+         * number noise.
+         */
+        noise_ultralight(NOISE_SOURCE_KEY, lParam);
+
+        /*
+         * We don't do TranslateMessage since it disassociates the
+         * resulting CHAR message from the KEYDOWN that sparked it,
+         * which we occasionally don't want. Instead, we process
+         * KEYDOWN, and call the Win32 translator functions so that
+         * we get the translations under _our_ control.
+         */
+        {
+            unsigned char buf[20];
+            int len;
+
+            if (wParam == VK_PROCESSKEY || /* IME PROCESS key */
+                wParam == VK_PACKET) {     /* 'this key is a Unicode char' */
+                if (message == WM_KEYDOWN) {
+                    MSG m;
+                    m.hwnd = hwnd;
+                    m.message = WM_KEYDOWN;
+                    m.wParam = wParam;
+                    m.lParam = lParam & 0xdfff;
+                    TranslateMessage(&m);
+                } else break; /* pass to Windows for default processing */
+            } else {
+                len = TranslateKey(message, wParam, lParam, buf);
+                if (len == -1)
+                    return sw_DefWindowProc(hwnd, message, wParam, lParam);
+
+                if (len != 0) {
+                    /*
+                     * We need not bother about stdin backlogs
+                     * here, because in GUI PuTTY we can't do
+                     * anything about it anyway; there's no means
+                     * of asking Windows to hold off on KEYDOWN
+                     * messages. We _have_ to buffer everything
+                     * we're sent.
+                     */
+                    term_keyinput(term, -1, buf, len);
+                    show_mouseptr(false);
+                }
+            }
+        }
+        return 0;
+      case WM_INPUTLANGCHANGE:
+        /* wParam == Font number */
+        /* lParam == Locale */
+        set_input_locale((HKL)lParam);
+        sys_cursor_update();
+        break;
+      case WM_IME_STARTCOMPOSITION: {
+        HIMC hImc = ImmGetContext(hwnd);
+        ImmSetCompositionFont(hImc, &lfont);
+        ImmReleaseContext(hwnd, hImc);
+        break;
+      }
+      case WM_IME_COMPOSITION: {
+        HIMC hIMC;
+        int n;
+        char *buff;
+
+        if (osPlatformId == VER_PLATFORM_WIN32_WINDOWS ||
+            osPlatformId == VER_PLATFORM_WIN32s)
+            break; /* no Unicode */
+
+        if ((lParam & GCS_RESULTSTR) == 0) /* Composition unfinished. */
+            break; /* fall back to DefWindowProc */
+
+        hIMC = ImmGetContext(hwnd);
+        n = ImmGetCompositionStringW(hIMC, GCS_RESULTSTR, NULL, 0);
+
+        if (n > 0) {
+            int i;
+            buff = snewn(n, char);
+            ImmGetCompositionStringW(hIMC, GCS_RESULTSTR, buff, n);
+            /*
+             * Jaeyoun Chung reports that Korean character
+             * input doesn't work correctly if we do a single
+             * term_keyinputw covering the whole of buff. So
+             * instead we send the characters one by one.
+             */
+            /* don't divide SURROGATE PAIR */
+            if (ldisc) {
+                for (i = 0; i < n; i += 2) {
+                    WCHAR hs = *(unsigned short *)(buff+i);
+                    if (IS_HIGH_SURROGATE(hs) && i+2 < n) {
+                        WCHAR ls = *(unsigned short *)(buff+i+2);
+                        if (IS_LOW_SURROGATE(ls)) {
+                            term_keyinputw(
+                                term, (unsigned short *)(buff+i), 2);
+                            i += 2;
+                            continue;
+                        }
+                    }
+                    term_keyinputw(
+                        term, (unsigned short *)(buff+i), 1);
+                }
+            }
+            free(buff);
+        }
+        ImmReleaseContext(hwnd, hIMC);
+        return 1;
+      }
+
+      case WM_IME_CHAR:
+        if (wParam & 0xFF00) {
+            char buf[2];
+
+            buf[1] = wParam;
+            buf[0] = wParam >> 8;
+            term_keyinput(term, kbd_codepage, buf, 2);
+        } else {
+            char c = (unsigned char) wParam;
+            term_seen_key_event(term);
+            term_keyinput(term, kbd_codepage, &c, 1);
+        }
+        return (0);
+      case WM_CHAR:
+      case WM_SYSCHAR:
+        /*
+         * Nevertheless, we are prepared to deal with WM_CHAR
+         * messages, should they crop up. So if someone wants to
+         * post the things to us as part of a macro manoeuvre,
+         * we're ready to cope.
+         */
+        if (unicode_window) {
+            static wchar_t pending_surrogate = 0;
+            wchar_t c = wParam;
+
+            if (IS_HIGH_SURROGATE(c)) {
+                pending_surrogate = c;
+            } else if (IS_SURROGATE_PAIR(pending_surrogate, c)) {
+                wchar_t pair[2];
+                pair[0] = pending_surrogate;
+                pair[1] = c;
+                term_keyinputw(term, pair, 2);
+            } else if (!IS_SURROGATE(c)) {
+                term_keyinputw(term, &c, 1);
+            }
+        } else {
+            char c = (unsigned char)wParam;
+            term_seen_key_event(term);
+            if (ldisc)
+                term_keyinput(term, CP_ACP, &c, 1);
+        }
+        return 0;
+      case WM_SYSCOLORCHANGE:
+        if (conf_get_bool(conf, CONF_system_colour)) {
+            /* Refresh palette from system colours. */
+            term_notify_palette_changed(term);
+            init_palette();
+            /* Force a repaint of the terminal window. */
+            term_invalidate(term);
+        }
+        break;
+      case WM_GOT_CLIPDATA:
+        process_clipdata((HGLOBAL)lParam, wParam);
+        return 0;
+      default:
+        if (message == wm_mousewheel || message == WM_MOUSEWHEEL
+                                                || message == WM_MOUSEHWHEEL) {
+            bool shift_pressed = false, control_pressed = false;
+
+            if (message == WM_MOUSEWHEEL || message == WM_MOUSEHWHEEL) {
+                wheel_accumulator += (short)HIWORD(wParam);
+                shift_pressed=LOWORD(wParam) & MK_SHIFT;
+                control_pressed=LOWORD(wParam) & MK_CONTROL;
+            } else {
+                BYTE keys[256];
+                wheel_accumulator += (int)wParam;
+                if (GetKeyboardState(keys)!=0) {
+                    shift_pressed=keys[VK_SHIFT]&0x80;
+                    control_pressed=keys[VK_CONTROL]&0x80;
+                }
+            }
+
+            /* process events when the threshold is reached */
+            while (abs(wheel_accumulator) >= WHEEL_DELTA) {
+                int b;
+
+                /* reduce amount for next time */
+                if (wheel_accumulator > 0) {
+                    b = message == WM_MOUSEHWHEEL ? MBT_WHEEL_RIGHT : MBT_WHEEL_UP;
+                    wheel_accumulator -= WHEEL_DELTA;
+                } else if (wheel_accumulator < 0) {
+                    b =  message == WM_MOUSEHWHEEL ? MBT_WHEEL_LEFT : MBT_WHEEL_DOWN;
+                    wheel_accumulator += WHEEL_DELTA;
+                } else
+                    break;
+
+                if (send_raw_mouse &&
+                    !(conf_get_bool(conf, CONF_mouse_override) &&
+                      shift_pressed)) {
+                    /* Mouse wheel position is in screen coordinates for
+                     * some reason */
+                    POINT p;
+                    p.x = X_POS(lParam); p.y = Y_POS(lParam);
+                    if (ScreenToClient(hwnd, &p)) {
+                        /* send a mouse-down followed by a mouse up */
+                        term_mouse(term, b, translate_button(b),
+                                   MA_CLICK,
+                                   TO_CHR_X(p.x),
+                                   TO_CHR_Y(p.y), shift_pressed,
+                                   control_pressed, is_alt_pressed());
+                    } /* else: not sure when this can fail */
+                } else if (message != WM_MOUSEHWHEEL) {
+                    /* trigger a scroll */
+                    term_scroll(term, 0,
+                                b == MBT_WHEEL_UP ?
+                                -term->rows / 2 : term->rows / 2);
+                }
+            }
+            return 0;
+        }
+    }
+
+    /*
+     * Any messages we don't process completely above are passed through to
+     * DefWindowProc() for default processing.
+     */
+    return sw_DefWindowProc(hwnd, message, wParam, lParam);
+}
+
+/*
+ * Move the system caret. (We maintain one, even though it's
+ * invisible, for the benefit of blind people: apparently some
+ * helper software tracks the system caret, so we should arrange to
+ * have one.)
+ */
+static void wintw_set_cursor_pos(TermWin *tw, int x, int y)
+{
+    int cx, cy;
+
+    if (!term->has_focus) return;
+
+    /*
+     * Avoid gratuitously re-updating the cursor position and IMM
+     * window if there's no actual change required.
+     */
+    cx = x * font_width + offset_width;
+    cy = y * font_height + offset_height;
+    if (cx == caret_x && cy == caret_y)
+        return;
+    caret_x = cx;
+    caret_y = cy;
+
+    sys_cursor_update();
+}
+
+static void sys_cursor_update(void)
+{
+    COMPOSITIONFORM cf;
+    HIMC hIMC;
+
+    if (!term->has_focus) return;
+
+    if (caret_x < 0 || caret_y < 0)
+        return;
+
+    SetCaretPos(caret_x, caret_y);
+
+    /* IMM calls on Win98 and beyond only */
+    if (osPlatformId == VER_PLATFORM_WIN32s) return; /* 3.11 */
+
+    if (osPlatformId == VER_PLATFORM_WIN32_WINDOWS &&
+        osMinorVersion == 0) return; /* 95 */
+
+    /* we should have the IMM functions */
+    hIMC = ImmGetContext(wgs.term_hwnd);
+    cf.dwStyle = CFS_POINT;
+    cf.ptCurrentPos.x = caret_x;
+    cf.ptCurrentPos.y = caret_y;
+    ImmSetCompositionWindow(hIMC, &cf);
+
+    ImmReleaseContext(wgs.term_hwnd, hIMC);
+}
+
+static void draw_horizontal_line_on_text(int y, int lattr, RECT line_box,
+                                         COLORREF colour)
+{
+    if (lattr == LATTR_TOP || lattr == LATTR_BOT) {
+        y *= 2;
+        if (lattr == LATTR_BOT)
+            y -= font_height;
+    }
+
+    if (!(0 <= y && y < font_height))
+        return;
+
+    HPEN oldpen = SelectObject(wintw_hdc, CreatePen(PS_SOLID, 0, colour));
+    MoveToEx(wintw_hdc, line_box.left, line_box.top + y, NULL);
+    LineTo(wintw_hdc, line_box.right, line_box.top + y);
+    oldpen = SelectObject(wintw_hdc, oldpen);
+    DeleteObject(oldpen);
+}
+
+/*
+ * Draw a line of text in the window, at given character
+ * coordinates, in given attributes.
+ *
+ * We are allowed to fiddle with the contents of `text'.
+ */
+static void do_text_internal(
+    int x, int y, wchar_t *text, int len,
+    unsigned long attr, int lattr, truecolour truecolour)
+{
+    COLORREF fg, bg, t;
+    int nfg, nbg, nfont;
+    RECT line_box;
+    bool force_manual_underline = false;
+    int fnt_width, char_width;
+    int text_adjust = 0;
+    int xoffset = 0;
+    int maxlen, remaining;
+    bool opaque;
+    bool is_cursor = false;
+    static int *lpDx = NULL;
+    static size_t lpDx_len = 0;
+    int *lpDx_maybe;
+    int len2; /* for SURROGATE PAIR */
+
+    lattr &= LATTR_MODE;
+
+    char_width = fnt_width = font_width * (1 + (lattr != LATTR_NORM));
+
+    if (attr & ATTR_WIDE)
+        char_width *= 2;
+
+    /* Only want the left half of double width lines */
+    if (lattr != LATTR_NORM && x*2 >= term->cols)
+        return;
+
+    x *= fnt_width;
+    y *= font_height;
+    x += offset_width;
+    y += offset_height;
+
+    if ((attr & TATTR_ACTCURS) && (cursor_type == 0 || term->big_cursor)) {
+        truecolour.fg = truecolour.bg = optionalrgb_none;
+        attr &= ~(ATTR_REVERSE|ATTR_BLINK|ATTR_COLOURS|ATTR_DIM);
+        /* cursor fg and bg */
+        attr |= (260 << ATTR_FGSHIFT) | (261 << ATTR_BGSHIFT);
+        is_cursor = true;
+    }
+
+    nfont = 0;
+    if (vtmode == VT_POORMAN && lattr != LATTR_NORM) {
+        /* Assume a poorman font is borken in other ways too. */
+        lattr = LATTR_WIDE;
+    } else
+        switch (lattr) {
+          case LATTR_NORM:
+            break;
+          case LATTR_WIDE:
+            nfont |= FONT_WIDE;
+            break;
+          default:
+            nfont |= FONT_WIDE + FONT_HIGH;
+            break;
+        }
+    if (attr & ATTR_NARROW)
+        nfont |= FONT_NARROW;
+
+#ifdef USES_VTLINE_HACK
+    /* Special hack for the VT100 linedraw glyphs. */
+    if (text[0] >= 0x23BA && text[0] <= 0x23BD) {
+        switch ((unsigned char) (text[0])) {
+          case 0xBA:
+            text_adjust = -2 * font_height / 5;
+            break;
+          case 0xBB:
+            text_adjust = -1 * font_height / 5;
+            break;
+          case 0xBC:
+            text_adjust = font_height / 5;
+            break;
+          case 0xBD:
+            text_adjust = 2 * font_height / 5;
+            break;
+        }
+        if (lattr == LATTR_TOP || lattr == LATTR_BOT)
+            text_adjust *= 2;
+        text[0] = ucsdata.unitab_xterm['q'];
+        if (attr & ATTR_UNDER) {
+            attr &= ~ATTR_UNDER;
+            force_manual_underline = true;
+        }
+    }
+#endif
+
+    /* Anything left as an original character set is unprintable. */
+    if (DIRECT_CHAR(text[0]) &&
+        (len < 2 || !IS_SURROGATE_PAIR(text[0], text[1]))) {
+        int i;
+        for (i = 0; i < len; i++)
+            text[i] = 0xFFFD;
+    }
+
+    /* OEM CP */
+    if ((text[0] & CSET_MASK) == CSET_OEMCP)
+        nfont |= FONT_OEM;
+
+    nfg = ((attr & ATTR_FGMASK) >> ATTR_FGSHIFT);
+    nbg = ((attr & ATTR_BGMASK) >> ATTR_BGSHIFT);
+    if (bold_font_mode == BOLD_FONT && (attr & ATTR_BOLD))
+        nfont |= FONT_BOLD;
+    if (und_mode == UND_FONT && (attr & ATTR_UNDER))
+        nfont |= FONT_UNDERLINE;
+    another_font(nfont);
+    if (!fonts[nfont]) {
+        if (nfont & FONT_UNDERLINE)
+            force_manual_underline = true;
+        /* Don't do the same for manual bold, it could be bad news. */
+
+        nfont &= ~(FONT_BOLD | FONT_UNDERLINE);
+    }
+    another_font(nfont);
+    if (!fonts[nfont])
+        nfont = FONT_NORMAL;
+    if (attr & ATTR_REVERSE) {
+        struct optionalrgb trgb;
+
+        t = nfg;
+        nfg = nbg;
+        nbg = t;
+
+        trgb = truecolour.fg;
+        truecolour.fg = truecolour.bg;
+        truecolour.bg = trgb;
+    }
+    if (bold_colours && (attr & ATTR_BOLD) && !is_cursor) {
+        if (nfg < 16) nfg |= 8;
+        else if (nfg >= 256) nfg |= 1;
+    }
+    if (bold_colours && (attr & ATTR_BLINK)) {
+        if (nbg < 16) nbg |= 8;
+        else if (nbg >= 256) nbg |= 1;
+    }
+    if (!pal && truecolour.fg.enabled)
+        fg = RGB(truecolour.fg.r, truecolour.fg.g, truecolour.fg.b);
+    else
+        fg = colours[nfg];
+
+    if (!pal && truecolour.bg.enabled)
+        bg = RGB(truecolour.bg.r, truecolour.bg.g, truecolour.bg.b);
+    else
+        bg = colours[nbg];
+
+    if (!pal && (attr & ATTR_DIM)) {
+        fg = RGB(GetRValue(fg) * 2 / 3,
+                 GetGValue(fg) * 2 / 3,
+                 GetBValue(fg) * 2 / 3);
+    }
+
+    SelectObject(wintw_hdc, fonts[nfont]);
+    SetTextColor(wintw_hdc, fg);
+    SetBkColor(wintw_hdc, bg);
+    if (attr & TATTR_COMBINING)
+        SetBkMode(wintw_hdc, TRANSPARENT);
+    else
+        SetBkMode(wintw_hdc, OPAQUE);
+    line_box.left = x;
+    line_box.top = y;
+    line_box.right = x + char_width * len;
+    line_box.bottom = y + font_height;
+    /* adjust line_box.right for SURROGATE PAIR & VARIATION SELECTOR */
+    {
+        int i;
+        int rc_width = 0;
+        for (i = 0; i < len ; i++) {
+            if (i+1 < len && IS_HIGH_VARSEL(text[i], text[i+1])) {
+                i++;
+            } else if (i+1 < len && IS_SURROGATE_PAIR(text[i], text[i+1])) {
+                rc_width += char_width;
+                i++;
+            } else if (IS_LOW_VARSEL(text[i])) {
+                /* do nothing */
+            } else {
+                rc_width += char_width;
+            }
+        }
+        line_box.right = line_box.left + rc_width;
+    }
+
+    /* Only want the left half of double width lines */
+    if (line_box.right > font_width*term->cols+offset_width)
+        line_box.right = font_width*term->cols+offset_width;
+
+    if (font_varpitch) {
+        /*
+         * If we're using a variable-pitch font, we unconditionally
+         * draw the glyphs one at a time and centre them in their
+         * character cells (which means in particular that we must
+         * disable the lpDx mechanism). This gives slightly odd but
+         * generally reasonable results.
+         */
+        xoffset = char_width / 2;
+        SetTextAlign(wintw_hdc, TA_TOP | TA_CENTER | TA_NOUPDATECP);
+        lpDx_maybe = NULL;
+        maxlen = 1;
+    } else {
+        /*
+         * In a fixed-pitch font, we draw the whole string in one go
+         * in the normal way.
+         */
+        xoffset = 0;
+        SetTextAlign(wintw_hdc, TA_TOP | TA_LEFT | TA_NOUPDATECP);
+        lpDx_maybe = lpDx;
+        maxlen = len;
+    }
+
+    opaque = true;                     /* start by erasing the rectangle */
+    for (remaining = len; remaining > 0;
+         text += len, remaining -= len, x += char_width * len2) {
+        len = (maxlen < remaining ? maxlen : remaining);
+        /* don't divide SURROGATE PAIR and VARIATION SELECTOR */
+        len2 = len;
+        if (maxlen == 1) {
+            if (remaining >= 1 && IS_SURROGATE_PAIR(text[0], text[1]))
+                len++;
+            if (remaining-len >= 1 && IS_LOW_VARSEL(text[len]))
+                len++;
+            else if (remaining-len >= 2 &&
+                     IS_HIGH_VARSEL(text[len], text[len+1]))
+                len += 2;
+        }
+
+        if (len > lpDx_len) {
+            sgrowarray(lpDx, lpDx_len, len);
+            if (lpDx_maybe) lpDx_maybe = lpDx;
+        }
+
+        {
+            int i;
+            /* only last char has dx width in SURROGATE PAIR and
+             * VARIATION sequence */
+            for (i = 0; i < len; i++) {
+                lpDx[i] = char_width;
+                if (i+1 < len && IS_HIGH_VARSEL(text[i], text[i+1])) {
+                    if (i > 0) lpDx[i-1] = 0;
+                    lpDx[i] = 0;
+                    i++;
+                    lpDx[i] = char_width;
+                } else if (i+1 < len && IS_SURROGATE_PAIR(text[i],text[i+1])) {
+                    lpDx[i] = 0;
+                    i++;
+                    lpDx[i] = char_width;
+                } else if (IS_LOW_VARSEL(text[i])) {
+                    if (i > 0) lpDx[i-1] = 0;
+                    lpDx[i] = char_width;
+                }
+            }
+        }
+
+        /* We're using a private area for direct to font. (512 chars.) */
+        if (ucsdata.dbcs_screenfont && (text[0] & CSET_MASK) == CSET_ACP) {
+            /* Ho Hum, dbcs fonts are a PITA! */
+            /* To display on W9x I have to convert to UCS */
+            static wchar_t *uni_buf = 0;
+            static int uni_len = 0;
+            int nlen, mptr;
+            if (len > uni_len) {
+                sfree(uni_buf);
+                uni_len = len;
+                uni_buf = snewn(uni_len, wchar_t);
+            }
+
+            for(nlen = mptr = 0; mptr<len; mptr++) {
+                uni_buf[nlen] = 0xFFFD;
+                if (IsDBCSLeadByteEx(ucsdata.font_codepage,
+                                     (BYTE) text[mptr])) {
+                    char dbcstext[2];
+                    dbcstext[0] = text[mptr] & 0xFF;
+                    dbcstext[1] = text[mptr+1] & 0xFF;
+                    lpDx[nlen] += char_width;
+                    MultiByteToWideChar(ucsdata.font_codepage, MB_USEGLYPHCHARS,
+                                        dbcstext, 2, uni_buf+nlen, 1);
+                    mptr++;
+                } else {
+                    char dbcstext[1];
+                    dbcstext[0] = text[mptr] & 0xFF;
+                    MultiByteToWideChar(ucsdata.font_codepage, MB_USEGLYPHCHARS,
+                                        dbcstext, 1, uni_buf+nlen, 1);
+                }
+                nlen++;
+            }
+            if (nlen <= 0)
+                return;                /* Eeek! */
+
+            ExtTextOutW(wintw_hdc, x + xoffset,
+                        y - font_height * (lattr == LATTR_BOT) + text_adjust,
+                        ETO_CLIPPED | (opaque ? ETO_OPAQUE : 0),
+                        &line_box, uni_buf, nlen,
+                        lpDx_maybe);
+            if (bold_font_mode == BOLD_SHADOW && (attr & ATTR_BOLD)) {
+                SetBkMode(wintw_hdc, TRANSPARENT);
+                ExtTextOutW(wintw_hdc, x + xoffset - 1,
+                            y - font_height * (lattr ==
+                                               LATTR_BOT) + text_adjust,
+                            ETO_CLIPPED, &line_box, uni_buf, nlen, lpDx_maybe);
+            }
+
+            lpDx[0] = -1;
+        } else if (DIRECT_FONT(text[0])) {
+            static char *directbuf = NULL;
+            static size_t directlen = 0;
+
+            sgrowarray(directbuf, directlen, len);
+            for (size_t i = 0; i < len; i++)
+                directbuf[i] = text[i] & 0xFF;
+
+            ExtTextOut(wintw_hdc, x + xoffset,
+                       y - font_height * (lattr == LATTR_BOT) + text_adjust,
+                       ETO_CLIPPED | (opaque ? ETO_OPAQUE : 0),
+                       &line_box, directbuf, len, lpDx_maybe);
+            if (bold_font_mode == BOLD_SHADOW && (attr & ATTR_BOLD)) {
+                SetBkMode(wintw_hdc, TRANSPARENT);
+
+                /* GRR: This draws the character outside its box and
+                 * can leave 'droppings' even with the clip box! I
+                 * suppose I could loop it one character at a time ...
+                 * yuk.
+                 *
+                 * Or ... I could do a test print with "W", and use +1
+                 * or -1 for this shift depending on if the leftmost
+                 * column is blank...
+                 */
+                ExtTextOut(wintw_hdc, x + xoffset - 1,
+                           y - font_height * (lattr ==
+                                              LATTR_BOT) + text_adjust,
+                           ETO_CLIPPED, &line_box, directbuf, len, lpDx_maybe);
+            }
+        } else {
+            /* And 'normal' unicode characters */
+            static WCHAR *wbuf = NULL;
+            static int wlen = 0;
+            int i;
+
+            if (wlen < len) {
+                sfree(wbuf);
+                wlen = len;
+                wbuf = snewn(wlen, WCHAR);
+            }
+
+            for (i = 0; i < len; i++)
+                wbuf[i] = text[i];
+
+            /* print Glyphs as they are, without Windows' Shaping*/
+            general_textout(wintw_hdc, x + xoffset,
+                            y - font_height * (lattr==LATTR_BOT) + text_adjust,
+                            &line_box, wbuf, len, lpDx,
+                            opaque && !(attr & TATTR_COMBINING));
+
+            /* And the shadow bold hack. */
+            if (bold_font_mode == BOLD_SHADOW && (attr & ATTR_BOLD)) {
+                SetBkMode(wintw_hdc, TRANSPARENT);
+                ExtTextOutW(wintw_hdc, x + xoffset - 1,
+                            y - font_height * (lattr ==
+                                               LATTR_BOT) + text_adjust,
+                            ETO_CLIPPED, &line_box, wbuf, len, lpDx_maybe);
+            }
+        }
+
+        /*
+         * If we're looping round again, stop erasing the background
+         * rectangle.
+         */
+        SetBkMode(wintw_hdc, TRANSPARENT);
+        opaque = false;
+    }
+
+    if (lattr != LATTR_TOP && (force_manual_underline ||
+                               (und_mode == UND_LINE && (attr & ATTR_UNDER))))
+        draw_horizontal_line_on_text(descent, lattr, line_box, fg);
+
+    if (attr & ATTR_STRIKE)
+        draw_horizontal_line_on_text(font_strikethrough_y, lattr, line_box, fg);
+}
+
+/*
+ * Wrapper that handles combining characters.
+ */
+static void wintw_draw_text(
+    TermWin *tw, int x, int y, wchar_t *text, int len,
+    unsigned long attr, int lattr, truecolour truecolour)
+{
+    if (attr & TATTR_COMBINING) {
+        unsigned long a = 0;
+        int len0 = 1;
+        /* don't divide SURROGATE PAIR and VARIATION SELECTOR */
+        if (len >= 2 && IS_SURROGATE_PAIR(text[0], text[1]))
+            len0 = 2;
+        if (len-len0 >= 1 && IS_LOW_VARSEL(text[len0])) {
+            attr &= ~TATTR_COMBINING;
+            do_text_internal(x, y, text, len0+1, attr, lattr, truecolour);
+            text += len0+1;
+            len -= len0+1;
+            a = TATTR_COMBINING;
+        } else if (len-len0 >= 2 && IS_HIGH_VARSEL(text[len0], text[len0+1])) {
+            attr &= ~TATTR_COMBINING;
+            do_text_internal(x, y, text, len0+2, attr, lattr, truecolour);
+            text += len0+2;
+            len -= len0+2;
+            a = TATTR_COMBINING;
+        } else {
+            attr &= ~TATTR_COMBINING;
+        }
+
+        while (len--) {
+            if (len >= 1 && IS_SURROGATE_PAIR(text[0], text[1])) {
+                do_text_internal(x, y, text, 2, attr | a, lattr, truecolour);
+                len--;
+                text++;
+            } else
+                do_text_internal(x, y, text, 1, attr | a, lattr, truecolour);
+
+            text++;
+            a = TATTR_COMBINING;
+        }
+    } else
+        do_text_internal(x, y, text, len, attr, lattr, truecolour);
+}
+
+static void wintw_draw_cursor(
+    TermWin *tw, int x, int y, wchar_t *text, int len,
+    unsigned long attr, int lattr, truecolour truecolour)
+{
+    int fnt_width;
+    int char_width;
+    int ctype = cursor_type;
+
+    lattr &= LATTR_MODE;
+
+    if ((attr & TATTR_ACTCURS) && (ctype == 0 || term->big_cursor)) {
+        if (*text != UCSWIDE) {
+            win_draw_text(tw, x, y, text, len, attr, lattr, truecolour);
+            return;
+        }
+        ctype = 2;
+        attr |= TATTR_RIGHTCURS;
+    }
+
+    fnt_width = char_width = font_width * (1 + (lattr != LATTR_NORM));
+    if (attr & ATTR_WIDE)
+        char_width *= 2;
+    x *= fnt_width;
+    y *= font_height;
+    x += offset_width;
+    y += offset_height;
+
+    if ((attr & TATTR_PASCURS) && (ctype == 0 || term->big_cursor)) {
+        POINT pts[5];
+        HPEN oldpen;
+        pts[0].x = pts[1].x = pts[4].x = x;
+        pts[2].x = pts[3].x = x + char_width - 1;
+        pts[0].y = pts[3].y = pts[4].y = y;
+        pts[1].y = pts[2].y = y + font_height - 1;
+        oldpen = SelectObject(wintw_hdc, CreatePen(PS_SOLID, 0, colours[261]));
+        Polyline(wintw_hdc, pts, 5);
+        oldpen = SelectObject(wintw_hdc, oldpen);
+        DeleteObject(oldpen);
+    } else if ((attr & (TATTR_ACTCURS | TATTR_PASCURS)) && ctype != 0) {
+        int startx, starty, dx, dy, length, i;
+        if (ctype == 1) {
+            startx = x;
+            starty = y + descent;
+            dx = 1;
+            dy = 0;
+            length = char_width;
+        } else {
+            int xadjust = 0;
+            if (attr & TATTR_RIGHTCURS)
+                xadjust = char_width - 1;
+            startx = x + xadjust;
+            starty = y;
+            dx = 0;
+            dy = 1;
+            length = font_height;
+        }
+        if (attr & TATTR_ACTCURS) {
+            HPEN oldpen;
+            oldpen =
+                SelectObject(wintw_hdc, CreatePen(PS_SOLID, 0, colours[261]));
+            MoveToEx(wintw_hdc, startx, starty, NULL);
+            LineTo(wintw_hdc, startx + dx * length, starty + dy * length);
+            oldpen = SelectObject(wintw_hdc, oldpen);
+            DeleteObject(oldpen);
+        } else {
+            for (i = 0; i < length; i++) {
+                if (i % 2 == 0) {
+                    SetPixel(wintw_hdc, startx, starty, colours[261]);
+                }
+                startx += dx;
+                starty += dy;
+            }
+        }
+    }
+}
+
+static void wintw_draw_trust_sigil(TermWin *tw, int x, int y)
+{
+    x *= font_width;
+    y *= font_height;
+    x += offset_width;
+    y += offset_height;
+
+    DrawIconEx(wintw_hdc, x, y, trust_icon, font_width * 2, font_height,
+               0, NULL, DI_NORMAL);
+}
+
+/* This function gets the actual width of a character in the normal font.
+ */
+static int wintw_char_width(TermWin *tw, int uc)
+{
+    int ibuf = 0;
+
+    /* If the font max is the same as the font ave width then this
+     * function is a no-op.
+     */
+    if (!font_dualwidth) return 1;
+
+    switch (uc & CSET_MASK) {
+      case CSET_ASCII:
+        uc = ucsdata.unitab_line[uc & 0xFF];
+        break;
+      case CSET_LINEDRW:
+        uc = ucsdata.unitab_xterm[uc & 0xFF];
+        break;
+      case CSET_SCOACS:
+        uc = ucsdata.unitab_scoacs[uc & 0xFF];
+        break;
+    }
+    if (DIRECT_FONT(uc)) {
+        if (ucsdata.dbcs_screenfont) return 1;
+
+        /* Speedup, I know of no font where ascii is the wrong width */
+        if ((uc&~CSET_MASK) >= ' ' && (uc&~CSET_MASK)<= '~')
+            return 1;
+
+        if ( (uc & CSET_MASK) == CSET_ACP ) {
+            SelectObject(wintw_hdc, fonts[FONT_NORMAL]);
+        } else if ( (uc & CSET_MASK) == CSET_OEMCP ) {
+            another_font(FONT_OEM);
+            if (!fonts[FONT_OEM]) return 0;
+
+            SelectObject(wintw_hdc, fonts[FONT_OEM]);
+        } else
+            return 0;
+
+        if (GetCharWidth32(wintw_hdc, uc & ~CSET_MASK,
+                           uc & ~CSET_MASK, &ibuf) != 1 &&
+            GetCharWidth(wintw_hdc, uc & ~CSET_MASK,
+                         uc & ~CSET_MASK, &ibuf) != 1)
+            return 0;
+    } else {
+        /* Speedup, I know of no font where ascii is the wrong width */
+        if (uc >= ' ' && uc <= '~') return 1;
+
+        SelectObject(wintw_hdc, fonts[FONT_NORMAL]);
+        if (GetCharWidth32W(wintw_hdc, uc, uc, &ibuf) == 1)
+            /* Okay that one worked */ ;
+        else if (GetCharWidthW(wintw_hdc, uc, uc, &ibuf) == 1)
+            /* This should work on 9x too, but it's "less accurate" */ ;
+        else
+            return 0;
+    }
+
+    ibuf += font_width / 2 -1;
+    ibuf /= font_width;
+
+    return ibuf;
+}
+
+DECL_WINDOWS_FUNCTION(static, BOOL, FlashWindowEx, (PFLASHWINFO));
+DECL_WINDOWS_FUNCTION(static, BOOL, ToUnicodeEx,
+                      (UINT, UINT, const BYTE *, LPWSTR, int, UINT, HKL));
+DECL_WINDOWS_FUNCTION(static, BOOL, PlaySound, (LPCTSTR, HMODULE, DWORD));
+
+static void init_winfuncs(void)
+{
+    HMODULE user32_module = load_system32_dll("user32.dll");
+    HMODULE winmm_module = load_system32_dll("winmm.dll");
+    HMODULE shcore_module = load_system32_dll("shcore.dll");
+    GET_WINDOWS_FUNCTION(user32_module, FlashWindowEx);
+    GET_WINDOWS_FUNCTION(user32_module, ToUnicodeEx);
+    GET_WINDOWS_FUNCTION_PP(winmm_module, PlaySound);
+    GET_WINDOWS_FUNCTION_NO_TYPECHECK(user32_module, GetMonitorInfoA);
+    GET_WINDOWS_FUNCTION_NO_TYPECHECK(user32_module, MonitorFromPoint);
+    GET_WINDOWS_FUNCTION_NO_TYPECHECK(user32_module, MonitorFromWindow);
+    GET_WINDOWS_FUNCTION_NO_TYPECHECK(shcore_module, GetDpiForMonitor);
+    GET_WINDOWS_FUNCTION_NO_TYPECHECK(user32_module, GetSystemMetricsForDpi);
+    GET_WINDOWS_FUNCTION_NO_TYPECHECK(user32_module, AdjustWindowRectExForDpi);
+}
+
+/*
+ * Translate a WM_(SYS)?KEY(UP|DOWN) message into a string of ASCII
+ * codes. Returns number of bytes used, zero to drop the message,
+ * -1 to forward the message to Windows, or another negative number
+ * to indicate a NUL-terminated "special" string.
+ */
+static int TranslateKey(UINT message, WPARAM wParam, LPARAM lParam,
+                        unsigned char *output)
+{
+    BYTE keystate[256];
+    int scan, shift_state;
+    bool left_alt = false, key_down;
+    int r, i;
+    unsigned char *p = output;
+    static int alt_sum = 0;
+    int funky_type = conf_get_int(conf, CONF_funky_type);
+    bool no_applic_k = conf_get_bool(conf, CONF_no_applic_k);
+    bool ctrlaltkeys = conf_get_bool(conf, CONF_ctrlaltkeys);
+    bool nethack_keypad = conf_get_bool(conf, CONF_nethack_keypad);
+    char keypad_key = '\0';
+
+    HKL kbd_layout = GetKeyboardLayout(0);
+
+    static wchar_t keys_unicode[3];
+    static int compose_char = 0;
+    static WPARAM compose_keycode = 0;
+
+    r = GetKeyboardState(keystate);
+    if (!r)
+        memset(keystate, 0, sizeof(keystate));
+    else {
+#if 0
+#define SHOW_TOASCII_RESULT
+        {                              /* Tell us all about key events */
+            static BYTE oldstate[256];
+            static int first = 1;
+            static int scan;
+            int ch;
+            if (first)
+                memcpy(oldstate, keystate, sizeof(oldstate));
+            first = 0;
+
+            if ((HIWORD(lParam) & (KF_UP | KF_REPEAT)) == KF_REPEAT) {
+                debug("+");
+            } else if ((HIWORD(lParam) & KF_UP)
+                       && scan == (HIWORD(lParam) & 0xFF)) {
+                debug(". U");
+            } else {
+                debug(".\n");
+                if (wParam >= VK_F1 && wParam <= VK_F20)
+                    debug("K_F%d", wParam + 1 - VK_F1);
+                else
+                    switch (wParam) {
+                      case VK_SHIFT:
+                        debug("SHIFT");
+                        break;
+                      case VK_CONTROL:
+                        debug("CTRL");
+                        break;
+                      case VK_MENU:
+                        debug("ALT");
+                        break;
+                      default:
+                        debug("VK_%02x", wParam);
+                    }
+                if (message == WM_SYSKEYDOWN || message == WM_SYSKEYUP)
+                    debug("*");
+                debug(", S%02x", scan = (HIWORD(lParam) & 0xFF));
+
+                ch = MapVirtualKeyEx(wParam, 2, kbd_layout);
+                if (ch >= ' ' && ch <= '~')
+                    debug(", '%c'", ch);
+                else if (ch)
+                    debug(", $%02x", ch);
+
+                if (keys_unicode[0])
+                    debug(", KB0=%04x", keys_unicode[0]);
+                if (keys_unicode[1])
+                    debug(", KB1=%04x", keys_unicode[1]);
+                if (keys_unicode[2])
+                    debug(", KB2=%04x", keys_unicode[2]);
+
+                if ((keystate[VK_SHIFT] & 0x80) != 0)
+                    debug(", S");
+                if ((keystate[VK_CONTROL] & 0x80) != 0)
+                    debug(", C");
+                if ((HIWORD(lParam) & KF_EXTENDED))
+                    debug(", E");
+                if ((HIWORD(lParam) & KF_UP))
+                    debug(", U");
+            }
+
+            if ((HIWORD(lParam) & (KF_UP | KF_REPEAT)) == KF_REPEAT);
+            else if ((HIWORD(lParam) & KF_UP))
+                oldstate[wParam & 0xFF] ^= 0x80;
+            else
+                oldstate[wParam & 0xFF] ^= 0x81;
+
+            for (ch = 0; ch < 256; ch++)
+                if (oldstate[ch] != keystate[ch])
+                    debug(", M%02x=%02x", ch, keystate[ch]);
+
+            memcpy(oldstate, keystate, sizeof(oldstate));
+        }
+#endif
+
+        if (wParam == VK_MENU && (HIWORD(lParam) & KF_EXTENDED)) {
+            keystate[VK_RMENU] = keystate[VK_MENU];
+        }
+
+
+        /* Nastiness with NUMLock - Shift-NUMLock is left alone though */
+        if ((funky_type == FUNKY_VT400 ||
+             (funky_type <= FUNKY_LINUX && term->app_keypad_keys &&
+              !no_applic_k))
+            && wParam == VK_NUMLOCK && !(keystate[VK_SHIFT] & 0x80)) {
+
+            wParam = VK_EXECUTE;
+
+            /* UnToggle NUMLock */
+            if ((HIWORD(lParam) & (KF_UP | KF_REPEAT)) == 0)
+                keystate[VK_NUMLOCK] ^= 1;
+        }
+
+        /* And write back the 'adjusted' state */
+        SetKeyboardState(keystate);
+    }
+
+    /* Disable Auto repeat if required */
+    if (term->repeat_off &&
+        (HIWORD(lParam) & (KF_UP | KF_REPEAT)) == KF_REPEAT)
+        return 0;
+
+    if ((HIWORD(lParam) & KF_ALTDOWN) && (keystate[VK_RMENU] & 0x80) == 0)
+        left_alt = true;
+
+    key_down = ((HIWORD(lParam) & KF_UP) == 0);
+
+    /* Make sure Ctrl-ALT is not the same as AltGr for ToAscii unless told. */
+    if (left_alt && (keystate[VK_CONTROL] & 0x80)) {
+        if (ctrlaltkeys)
+            keystate[VK_MENU] = 0;
+        else {
+            keystate[VK_RMENU] = 0x80;
+            left_alt = false;
+        }
+    }
+
+    scan = (HIWORD(lParam) & (KF_UP | KF_EXTENDED | 0xFF));
+    shift_state = ((keystate[VK_SHIFT] & 0x80) != 0)
+        + ((keystate[VK_CONTROL] & 0x80) != 0) * 2;
+
+    /* Note if AltGr was pressed and if it was used as a compose key */
+    if (!compose_state) {
+        compose_keycode = 0x100;
+        if (conf_get_bool(conf, CONF_compose_key)) {
+            if (wParam == VK_MENU && (HIWORD(lParam) & KF_EXTENDED))
+                compose_keycode = wParam;
+        }
+        if (wParam == VK_APPS)
+            compose_keycode = wParam;
+    }
+
+    if (wParam == compose_keycode) {
+        if (compose_state == 0 &&
+            (HIWORD(lParam) & (KF_UP | KF_REPEAT)) == 0)
+            compose_state = 1;
+        else if (compose_state == 1 && (HIWORD(lParam) & KF_UP))
+            compose_state = 2;
+        else
+            compose_state = 0;
+    } else if (compose_state == 1 && wParam != VK_CONTROL)
+        compose_state = 0;
+
+    if (compose_state > 1 && left_alt)
+        compose_state = 0;
+
+    /* Sanitize the number pad if not using a PC NumPad */
+    if (left_alt || (term->app_keypad_keys && !no_applic_k
+                     && funky_type != FUNKY_XTERM)
+        || funky_type == FUNKY_VT400 || nethack_keypad || compose_state) {
+        if ((HIWORD(lParam) & KF_EXTENDED) == 0) {
+            int nParam = 0;
+            switch (wParam) {
+              case VK_INSERT:
+                nParam = VK_NUMPAD0;
+                break;
+              case VK_END:
+                nParam = VK_NUMPAD1;
+                break;
+              case VK_DOWN:
+                nParam = VK_NUMPAD2;
+                break;
+              case VK_NEXT:
+                nParam = VK_NUMPAD3;
+                break;
+              case VK_LEFT:
+                nParam = VK_NUMPAD4;
+                break;
+              case VK_CLEAR:
+                nParam = VK_NUMPAD5;
+                break;
+              case VK_RIGHT:
+                nParam = VK_NUMPAD6;
+                break;
+              case VK_HOME:
+                nParam = VK_NUMPAD7;
+                break;
+              case VK_UP:
+                nParam = VK_NUMPAD8;
+                break;
+              case VK_PRIOR:
+                nParam = VK_NUMPAD9;
+                break;
+              case VK_DELETE:
+                nParam = VK_DECIMAL;
+                break;
+            }
+            if (nParam) {
+                if (keystate[VK_NUMLOCK] & 1)
+                    shift_state |= 1;
+                wParam = nParam;
+            }
+        }
+    }
+
+    /* If a key is pressed and AltGr is not active */
+    if (key_down && (keystate[VK_RMENU] & 0x80) == 0 && !compose_state) {
+        /* Okay, prepare for most alts then ... */
+        if (left_alt)
+            *p++ = '\033';
+
+        /* Lets see if it's a pattern we know all about ... */
+        if (wParam == VK_PRIOR && shift_state == 1) {
+            SendMessage(wgs.term_hwnd, WM_VSCROLL, SB_PAGEUP, 0);
+            return 0;
+        }
+        if (wParam == VK_PRIOR && shift_state == 3) { /* ctrl-shift-pageup */
+            SendMessage(wgs.term_hwnd, WM_VSCROLL, SB_TOP, 0);
+            return 0;
+        }
+        if (wParam == VK_NEXT && shift_state == 3) { /* ctrl-shift-pagedown */
+            SendMessage(wgs.term_hwnd, WM_VSCROLL, SB_BOTTOM, 0);
+            return 0;
+        }
+
+        if (wParam == VK_PRIOR && shift_state == 2) {
+            SendMessage(wgs.term_hwnd, WM_VSCROLL, SB_LINEUP, 0);
+            return 0;
+        }
+        if (wParam == VK_NEXT && shift_state == 1) {
+            SendMessage(wgs.term_hwnd, WM_VSCROLL, SB_PAGEDOWN, 0);
+            return 0;
+        }
+        if (wParam == VK_NEXT && shift_state == 2) {
+            SendMessage(wgs.term_hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
+            return 0;
+        }
+        if ((wParam == VK_PRIOR || wParam == VK_NEXT) && shift_state == 3) {
+            term_scroll_to_selection(term, (wParam == VK_PRIOR ? 0 : 1));
+            return 0;
+        }
+        if (wParam == VK_INSERT && shift_state == 2) {
+            switch (conf_get_int(conf, CONF_ctrlshiftins)) {
+              case CLIPUI_IMPLICIT:
+                break;          /* no need to re-copy to CLIP_LOCAL */
+              case CLIPUI_EXPLICIT:
+                term_request_copy(term, clips_system, lenof(clips_system));
+                break;
+              default:
+                break;
+            }
+            return 0;
+        }
+        if (wParam == VK_INSERT && shift_state == 1) {
+            switch (conf_get_int(conf, CONF_ctrlshiftins)) {
+              case CLIPUI_IMPLICIT:
+                term_request_paste(term, CLIP_LOCAL);
+                break;
+              case CLIPUI_EXPLICIT:
+                term_request_paste(term, CLIP_SYSTEM);
+                break;
+              default:
+                break;
+            }
+            return 0;
+        }
+        if (wParam == 'C' && shift_state == 3) {
+            switch (conf_get_int(conf, CONF_ctrlshiftcv)) {
+              case CLIPUI_IMPLICIT:
+                break;          /* no need to re-copy to CLIP_LOCAL */
+              case CLIPUI_EXPLICIT:
+                term_request_copy(term, clips_system, lenof(clips_system));
+                break;
+              default:
+                break;
+            }
+            return 0;
+        }
+        if (wParam == 'V' && shift_state == 3) {
+            switch (conf_get_int(conf, CONF_ctrlshiftcv)) {
+              case CLIPUI_IMPLICIT:
+                term_request_paste(term, CLIP_LOCAL);
+                break;
+              case CLIPUI_EXPLICIT:
+                term_request_paste(term, CLIP_SYSTEM);
+                break;
+              default:
+                break;
+            }
+            return 0;
+        }
+        if (left_alt && wParam == VK_F4 && conf_get_bool(conf, CONF_alt_f4)) {
+            return -1;
+        }
+        if (left_alt && wParam == VK_SPACE && conf_get_bool(conf,
+                                                            CONF_alt_space)) {
+            SendMessage(wgs.term_hwnd, WM_SYSCOMMAND, SC_KEYMENU, 0);
+            return -1;
+        }
+        if (left_alt && wParam == VK_RETURN &&
+            conf_get_bool(conf, CONF_fullscreenonaltenter) &&
+            (conf_get_int(conf, CONF_resize_action) != RESIZE_DISABLED)) {
+            if ((HIWORD(lParam) & (KF_UP | KF_REPEAT)) != KF_REPEAT)
+                flip_full_screen();
+            return -1;
+        }
+        /* Control-Numlock for app-keypad mode switch */
+        if (wParam == VK_PAUSE && shift_state == 2) {
+            term->app_keypad_keys = !term->app_keypad_keys;
+            return 0;
+        }
+
+        if (wParam == VK_BACK && shift_state == 0) {    /* Backspace */
+            *p++ = (conf_get_bool(conf, CONF_bksp_is_delete) ? 0x7F : 0x08);
+            *p++ = 0;
+            return -2;
+        }
+        if (wParam == VK_BACK && shift_state == 1) {    /* Shift Backspace */
+            /* We do the opposite of what is configured */
+            *p++ = (conf_get_bool(conf, CONF_bksp_is_delete) ? 0x08 : 0x7F);
+            *p++ = 0;
+            return -2;
+        }
+        if (wParam == VK_TAB && shift_state == 1) {     /* Shift tab */
+            *p++ = 0x1B;
+            *p++ = '[';
+            *p++ = 'Z';
+            return p - output;
+        }
+        if (wParam == VK_SPACE && shift_state == 2) {   /* Ctrl-Space */
+            *p++ = 0;
+            return p - output;
+        }
+        if (wParam == VK_SPACE && shift_state == 3) {   /* Ctrl-Shift-Space */
+            *p++ = 160;
+            return p - output;
+        }
+        if (wParam == VK_CANCEL && shift_state == 2) {  /* Ctrl-Break */
+            if (backend)
+                backend_special(backend, SS_BRK, 0);
+            return 0;
+        }
+        if (wParam == VK_PAUSE) {      /* Break/Pause */
+            *p++ = 26;
+            *p++ = 0;
+            return -2;
+        }
+        /* Control-2 to Control-8 are special */
+        if (shift_state == 2 && wParam >= '2' && wParam <= '8') {
+            *p++ = "\000\033\034\035\036\037\177"[wParam - '2'];
+            return p - output;
+        }
+        if (shift_state == 2 && (wParam == 0xBD || wParam == 0xBF)) {
+            *p++ = 0x1F;
+            return p - output;
+        }
+        if (shift_state == 2 && (wParam == 0xDF || wParam == 0xDC)) {
+            *p++ = 0x1C;
+            return p - output;
+        }
+        if (shift_state == 3 && wParam == 0xDE) {
+            *p++ = 0x1E;               /* Ctrl-~ == Ctrl-^ in xterm at least */
+            return p - output;
+        }
+
+        switch (wParam) {
+            bool consumed_alt;
+
+          case VK_NUMPAD0: keypad_key = '0'; goto numeric_keypad;
+          case VK_NUMPAD1: keypad_key = '1'; goto numeric_keypad;
+          case VK_NUMPAD2: keypad_key = '2'; goto numeric_keypad;
+          case VK_NUMPAD3: keypad_key = '3'; goto numeric_keypad;
+          case VK_NUMPAD4: keypad_key = '4'; goto numeric_keypad;
+          case VK_NUMPAD5: keypad_key = '5'; goto numeric_keypad;
+          case VK_NUMPAD6: keypad_key = '6'; goto numeric_keypad;
+          case VK_NUMPAD7: keypad_key = '7'; goto numeric_keypad;
+          case VK_NUMPAD8: keypad_key = '8'; goto numeric_keypad;
+          case VK_NUMPAD9: keypad_key = '9'; goto numeric_keypad;
+          case VK_DECIMAL: keypad_key = '.'; goto numeric_keypad;
+          case VK_ADD: keypad_key = '+'; goto numeric_keypad;
+          case VK_SUBTRACT: keypad_key = '-'; goto numeric_keypad;
+          case VK_MULTIPLY: keypad_key = '*'; goto numeric_keypad;
+          case VK_DIVIDE: keypad_key = '/'; goto numeric_keypad;
+          case VK_EXECUTE: keypad_key = 'G'; goto numeric_keypad;
+            /* also the case for VK_RETURN below can sometimes come here */
+          numeric_keypad:
+            /* Left Alt overrides all numeric keypad usage to act as
+             * numeric character code input */
+            if (left_alt) {
+                if (keypad_key >= '0' && keypad_key <= '9')
+                    alt_sum = alt_sum * 10 + keypad_key - '0';
+                else
+                    alt_sum = 0;
+                break;
+            }
+
+            {
+                int nchars = format_numeric_keypad_key(
+                    (char *)p, term, keypad_key,
+                    shift_state & 1, shift_state & 2);
+                if (!nchars) {
+                    /*
+                     * If we didn't get an escape sequence out of the
+                     * numeric keypad key, then that must be because
+                     * we're in Num Lock mode without application
+                     * keypad enabled. In that situation we leave this
+                     * keypress to the ToUnicode/ToAsciiEx handler
+                     * below, which will translate it according to the
+                     * appropriate keypad layout (e.g. so that what a
+                     * Brit thinks of as keypad '.' can become ',' in
+                     * the German layout).
+                     *
+                     * An exception is the keypad Return key: if we
+                     * didn't get an escape sequence for that, we
+                     * treat it like ordinary Return, taking into
+                     * account Telnet special new line codes and
+                     * config options.
+                     */
+                    if (keypad_key == '\r')
+                        goto ordinary_return_key;
+                    break;
+                }
+
+                p += nchars;
+                return p - output;
+            }
+
+            int fkey_number;
+          case VK_F1: fkey_number = 1; goto numbered_function_key;
+          case VK_F2: fkey_number = 2; goto numbered_function_key;
+          case VK_F3: fkey_number = 3; goto numbered_function_key;
+          case VK_F4: fkey_number = 4; goto numbered_function_key;
+          case VK_F5: fkey_number = 5; goto numbered_function_key;
+          case VK_F6: fkey_number = 6; goto numbered_function_key;
+          case VK_F7: fkey_number = 7; goto numbered_function_key;
+          case VK_F8: fkey_number = 8; goto numbered_function_key;
+          case VK_F9: fkey_number = 9; goto numbered_function_key;
+          case VK_F10: fkey_number = 10; goto numbered_function_key;
+          case VK_F11: fkey_number = 11; goto numbered_function_key;
+          case VK_F12: fkey_number = 12; goto numbered_function_key;
+          case VK_F13: fkey_number = 13; goto numbered_function_key;
+          case VK_F14: fkey_number = 14; goto numbered_function_key;
+          case VK_F15: fkey_number = 15; goto numbered_function_key;
+          case VK_F16: fkey_number = 16; goto numbered_function_key;
+          case VK_F17: fkey_number = 17; goto numbered_function_key;
+          case VK_F18: fkey_number = 18; goto numbered_function_key;
+          case VK_F19: fkey_number = 19; goto numbered_function_key;
+          case VK_F20: fkey_number = 20; goto numbered_function_key;
+          numbered_function_key:
+            consumed_alt = false;
+            p += format_function_key((char *)p, term, fkey_number,
+                                     shift_state & 1, shift_state & 2,
+                                     left_alt, &consumed_alt);
+            if (consumed_alt)
+                left_alt = false; /* supersedes the usual prefixing of Esc */
+            return p - output;
+
+            SmallKeypadKey sk_key;
+          case VK_HOME: sk_key = SKK_HOME; goto small_keypad_key;
+          case VK_END: sk_key = SKK_END; goto small_keypad_key;
+          case VK_INSERT: sk_key = SKK_INSERT; goto small_keypad_key;
+          case VK_DELETE: sk_key = SKK_DELETE; goto small_keypad_key;
+          case VK_PRIOR: sk_key = SKK_PGUP; goto small_keypad_key;
+          case VK_NEXT: sk_key = SKK_PGDN; goto small_keypad_key;
+          small_keypad_key:
+            /* These keys don't generate terminal input with Ctrl */
+            if (shift_state & 2)
+                break;
+
+            p += format_small_keypad_key((char *)p, term, sk_key,
+                                         shift_state & 1, shift_state & 2,
+                                         left_alt, &consumed_alt);
+            if (consumed_alt)
+                left_alt = false; /* supersedes the usual prefixing of Esc */
+            return p - output;
+
+            char xkey;
+          case VK_UP: xkey = 'A'; goto arrow_key;
+          case VK_DOWN: xkey = 'B'; goto arrow_key;
+          case VK_RIGHT: xkey = 'C'; goto arrow_key;
+          case VK_LEFT: xkey = 'D'; goto arrow_key;
+          case VK_CLEAR: xkey = 'G'; goto arrow_key; /* close enough */
+          arrow_key:
+            consumed_alt = false;
+            p += format_arrow_key((char *)p, term, xkey, shift_state & 1,
+                                  shift_state & 2, left_alt, &consumed_alt);
+            if (consumed_alt)
+                left_alt = false; /* supersedes the usual prefixing of Esc */
+            return p - output;
+
+          case VK_RETURN:
+            if (HIWORD(lParam) & KF_EXTENDED) {
+                keypad_key = '\r';
+                goto numeric_keypad;
+            }
+          ordinary_return_key:
+            if (shift_state == 0 && term->cr_lf_return) {
+                *p++ = '\r';
+                *p++ = '\n';
+                return p - output;
+            } else {
+                *p++ = 0x0D;
+                *p++ = 0;
+                return -2;
+            }
+        }
+    }
+
+    /* Okay we've done everything interesting; let windows deal with
+     * the boring stuff */
+    {
+        bool capsOn = false;
+
+        /* helg: clear CAPS LOCK state if caps lock switches to cyrillic */
+        if(keystate[VK_CAPITAL] != 0 &&
+           conf_get_bool(conf, CONF_xlat_capslockcyr)) {
+            capsOn= !left_alt;
+            keystate[VK_CAPITAL] = 0;
+        }
+
+        /* XXX how do we know what the max size of the keys array should
+         * be is? There's indication on MS' website of an Inquire/InquireEx
+         * functioning returning a KBINFO structure which tells us. */
+        if (osPlatformId == VER_PLATFORM_WIN32_NT && p_ToUnicodeEx) {
+            r = p_ToUnicodeEx(wParam, scan, keystate, keys_unicode,
+                              lenof(keys_unicode), 0, kbd_layout);
+        } else {
+            /* XXX 'keys' parameter is declared in MSDN documentation as
+             * 'LPWORD lpChar'.
+             * The experience of a French user indicates that on
+             * Win98, WORD[] should be passed in, but on Win2K, it should
+             * be BYTE[]. German WinXP and my Win2K with "US International"
+             * driver corroborate this.
+             * Experimentally I've conditionalised the behaviour on the
+             * Win9x/NT split, but I suspect it's worse than that.
+             * See wishlist item `win-dead-keys' for more horrible detail
+             * and speculations. */
+            int i;
+            static WORD keys[3];
+            static BYTE keysb[3];
+            r = ToAsciiEx(wParam, scan, keystate, keys, 0, kbd_layout);
+            if (r > 0) {
+                for (i = 0; i < r; i++) {
+                    keysb[i] = (BYTE)keys[i];
+                }
+                MultiByteToWideChar(CP_ACP, 0, (LPCSTR)keysb, r,
+                                    keys_unicode, lenof(keys_unicode));
+            }
+        }
+#ifdef SHOW_TOASCII_RESULT
+        if (r == 1 && !key_down) {
+            if (alt_sum) {
+                if (in_utf(term) || ucsdata.dbcs_screenfont)
+                    debug(", (U+%04x)", alt_sum);
+                else
+                    debug(", LCH(%d)", alt_sum);
+            } else {
+                debug(", ACH(%d)", keys_unicode[0]);
+            }
+        } else if (r > 0) {
+            int r1;
+            debug(", ASC(");
+            for (r1 = 0; r1 < r; r1++) {
+                debug("%s%d", r1 ? "," : "", keys_unicode[r1]);
+            }
+            debug(")");
+        }
+#endif
+        if (r > 0) {
+            WCHAR keybuf;
+
+            p = output;
+            for (i = 0; i < r; i++) {
+                wchar_t wch = keys_unicode[i];
+
+                if (compose_state == 2 && wch >= ' ' && wch < 0x80) {
+                    compose_char = wch;
+                    compose_state++;
+                    continue;
+                }
+                if (compose_state == 3 && wch >= ' ' && wch < 0x80) {
+                    int nc;
+                    compose_state = 0;
+
+                    if ((nc = check_compose(compose_char, wch)) == -1) {
+                        MessageBeep(MB_ICONHAND);
+                        return 0;
+                    }
+                    keybuf = nc;
+                    term_keyinputw(term, &keybuf, 1);
+                    continue;
+                }
+
+                compose_state = 0;
+
+                if (!key_down) {
+                    if (alt_sum) {
+                        if (in_utf(term) || ucsdata.dbcs_screenfont) {
+                            keybuf = alt_sum;
+                            term_keyinputw(term, &keybuf, 1);
+                        } else {
+                            char ch = (char) alt_sum;
+                            /*
+                             * We need not bother about stdin
+                             * backlogs here, because in GUI PuTTY
+                             * we can't do anything about it
+                             * anyway; there's no means of asking
+                             * Windows to hold off on KEYDOWN
+                             * messages. We _have_ to buffer
+                             * everything we're sent.
+                             */
+                            term_keyinput(term, -1, &ch, 1);
+                        }
+                        alt_sum = 0;
+                    } else {
+                        term_keyinputw(term, &wch, 1);
+                    }
+                } else {
+                    if(capsOn && wch < 0x80) {
+                        WCHAR cbuf[2];
+                        cbuf[0] = 27;
+                        cbuf[1] = xlat_uskbd2cyrllic(wch);
+                        term_keyinputw(term, cbuf+!left_alt, 1+!!left_alt);
+                    } else {
+                        WCHAR cbuf[2];
+                        cbuf[0] = '\033';
+                        cbuf[1] = wch;
+                        term_keyinputw(term, cbuf +!left_alt, 1+!!left_alt);
+                    }
+                }
+                show_mouseptr(false);
+            }
+
+            /* This is so the ALT-Numpad and dead keys work correctly. */
+            keys_unicode[0] = 0;
+
+            return p - output;
+        }
+        /* If we're definitely not building up an ALT-54321 then clear it */
+        if (!left_alt)
+            keys_unicode[0] = 0;
+        /* If we will be using alt_sum fix the 256s */
+        else if (keys_unicode[0] && (in_utf(term) || ucsdata.dbcs_screenfont))
+            keys_unicode[0] = 10;
+    }
+
+    /*
+     * ALT alone may or may not want to bring up the System menu.
+     * If it's not meant to, we return 0 on presses or releases of
+     * ALT, to show that we've swallowed the keystroke. Otherwise
+     * we return -1, which means Windows will give the keystroke
+     * its default handling (i.e. bring up the System menu).
+     */
+    if (wParam == VK_MENU && !conf_get_bool(conf, CONF_alt_only))
+        return 0;
+
+    return -1;
+}
+
+static void wintw_set_title(TermWin *tw, const char *title, int codepage)
+{
+    wchar_t *new_window_name = dup_mb_to_wc(codepage, 0, title);
+    if (!wcscmp(new_window_name, window_name)) {
+        sfree(new_window_name);
+        return;
+    }
+    sfree(window_name);
+    window_name = new_window_name;
+    if (conf_get_bool(conf, CONF_win_name_always) || !IsIconic(wgs.term_hwnd))
+        sw_SetWindowText(wgs.term_hwnd, window_name);
+}
+
+static void wintw_set_icon_title(TermWin *tw, const char *title, int codepage)
+{
+    wchar_t *new_icon_name = dup_mb_to_wc(codepage, 0, title);
+    if (!wcscmp(new_icon_name, icon_name)) {
+        sfree(new_icon_name);
+        return;
+    }
+    sfree(icon_name);
+    icon_name = new_icon_name;
+    if (!conf_get_bool(conf, CONF_win_name_always) && IsIconic(wgs.term_hwnd))
+        sw_SetWindowText(wgs.term_hwnd, icon_name);
+}
+
+static void wintw_set_scrollbar(TermWin *tw, int total, int start, int page)
+{
+    SCROLLINFO si;
+
+    if (!conf_get_bool(conf, is_full_screen() ?
+                       CONF_scrollbar_in_fullscreen : CONF_scrollbar))
+        return;
+
+    si.cbSize = sizeof(si);
+    si.fMask = SIF_ALL | SIF_DISABLENOSCROLL;
+    si.nMin = 0;
+    si.nMax = total - 1;
+    si.nPage = page;
+    si.nPos = start;
+    if (wgs.term_hwnd)
+        SetScrollInfo(wgs.term_hwnd, SB_VERT, &si, true);
+}
+
+static bool wintw_setup_draw_ctx(TermWin *tw)
+{
+    assert(!wintw_hdc);
+    wintw_hdc = make_hdc();
+    return wintw_hdc != NULL;
+}
+
+static void wintw_free_draw_ctx(TermWin *tw)
+{
+    assert(wintw_hdc);
+    free_hdc(wintw_hdc);
+    wintw_hdc = NULL;
+}
+
+/*
+ * Set up the colour palette.
+ */
+static void init_palette(void)
+{
+    pal = NULL;
+    logpal = snew_plus(LOGPALETTE, (OSC4_NCOLOURS - 1) * sizeof(PALETTEENTRY));
+    logpal->palVersion = 0x300;
+    logpal->palNumEntries = OSC4_NCOLOURS;
+    for (unsigned i = 0; i < OSC4_NCOLOURS; i++)
+        logpal->palPalEntry[i].peFlags = PC_NOCOLLAPSE;
+}
+
+static void wintw_palette_set(TermWin *win, unsigned start,
+                              unsigned ncolours, const rgb *colours_in)
+{
+    assert(start <= OSC4_NCOLOURS);
+    assert(ncolours <= OSC4_NCOLOURS - start);
+
+    for (unsigned i = 0; i < ncolours; i++) {
+        const rgb *in = &colours_in[i];
+        PALETTEENTRY *out = &logpal->palPalEntry[i + start];
+        out->peRed = in->r;
+        out->peGreen = in->g;
+        out->peBlue = in->b;
+        colours[i + start] = RGB(in->r, in->g, in->b) ^ colorref_modifier;
+    }
+
+    bool got_new_palette = false;
+
+    if (!tried_pal && conf_get_bool(conf, CONF_try_palette)) {
+        HDC hdc = GetDC(wgs.term_hwnd);
+        if (GetDeviceCaps(hdc, RASTERCAPS) & RC_PALETTE) {
+            pal = CreatePalette(logpal);
+            if (pal) {
+                SelectPalette(hdc, pal, false);
+                RealizePalette(hdc);
+                SelectPalette(hdc, GetStockObject(DEFAULT_PALETTE), false);
+
+                /* Convert all RGB() values in colours[] into PALETTERGB(),
+                 * and ensure we stick to that later */
+                colorref_modifier = PALETTERGB(0, 0, 0) ^ RGB(0, 0, 0);
+                for (unsigned i = 0; i < OSC4_NCOLOURS; i++)
+                    colours[i] ^= colorref_modifier;
+
+                /* Inhibit the SetPaletteEntries call below */
+                got_new_palette = true;
+            }
+        }
+        ReleaseDC(wgs.term_hwnd, hdc);
+        tried_pal = true;
+    }
+
+    if (pal && !got_new_palette) {
+        /* We already had a palette, so replace the changed colours in the
+         * existing one. */
+        SetPaletteEntries(pal, start, ncolours, logpal->palPalEntry + start);
+
+        HDC hdc = make_hdc();
+        UnrealizeObject(pal);
+        RealizePalette(hdc);
+        free_hdc(hdc);
+    }
+
+    if (start <= OSC4_COLOUR_bg && OSC4_COLOUR_bg < start + ncolours) {
+        /* If Default Background changes, we need to ensure any space between
+         * the text area and the window border is redrawn. */
+        InvalidateRect(wgs.term_hwnd, NULL, true);
+    }
+}
+
+void write_aclip(int clipboard, char *data, int len, bool must_deselect)
+{
+    HGLOBAL clipdata;
+    void *lock;
+
+    if (clipboard != CLIP_SYSTEM)
+        return;
+
+    clipdata = GlobalAlloc(GMEM_DDESHARE | GMEM_MOVEABLE, len + 1);
+    if (!clipdata)
+        return;
+    lock = GlobalLock(clipdata);
+    if (!lock)
+        return;
+    memcpy(lock, data, len);
+    ((unsigned char *) lock)[len] = 0;
+    GlobalUnlock(clipdata);
+
+    if (!must_deselect)
+        SendMessage(wgs.term_hwnd, WM_IGNORE_CLIP, true, 0);
+
+    if (OpenClipboard(wgs.term_hwnd)) {
+        EmptyClipboard();
+        SetClipboardData(CF_TEXT, clipdata);
+        CloseClipboard();
+    } else
+        GlobalFree(clipdata);
+
+    if (!must_deselect)
+        SendMessage(wgs.term_hwnd, WM_IGNORE_CLIP, false, 0);
+}
+
+typedef struct _rgbindex {
+    int index;
+    COLORREF ref;
+} rgbindex;
+
+int cmpCOLORREF(void *va, void *vb)
+{
+    COLORREF a = ((rgbindex *)va)->ref;
+    COLORREF b = ((rgbindex *)vb)->ref;
+    return (a < b) ? -1 : (a > b) ? +1 : 0;
+}
+
+/*
+ * Note: unlike write_aclip() this will not append a nul.
+ */
+static void wintw_clip_write(
+    TermWin *tw, int clipboard, wchar_t *data, int *attr,
+    truecolour *truecolour, int len, bool must_deselect)
+{
+    HGLOBAL clipdata, clipdata2, clipdata3;
+    int len2;
+    void *lock, *lock2, *lock3;
+
+    if (clipboard != CLIP_SYSTEM)
+        return;
+
+    len2 = WideCharToMultiByte(CP_ACP, 0, data, len, 0, 0, NULL, NULL);
+
+    clipdata = GlobalAlloc(GMEM_DDESHARE | GMEM_MOVEABLE,
+                           len * sizeof(wchar_t));
+    clipdata2 = GlobalAlloc(GMEM_DDESHARE | GMEM_MOVEABLE, len2);
+
+    if (!clipdata || !clipdata2) {
+        if (clipdata)
+            GlobalFree(clipdata);
+        if (clipdata2)
+            GlobalFree(clipdata2);
+        return;
+    }
+    if (!(lock = GlobalLock(clipdata))) {
+        GlobalFree(clipdata);
+        GlobalFree(clipdata2);
+        return;
+    }
+    if (!(lock2 = GlobalLock(clipdata2))) {
+        GlobalUnlock(clipdata);
+        GlobalFree(clipdata);
+        GlobalFree(clipdata2);
+        return;
+    }
+
+    memcpy(lock, data, len * sizeof(wchar_t));
+    WideCharToMultiByte(CP_ACP, 0, data, len, lock2, len2, NULL, NULL);
+
+    if (conf_get_bool(conf, CONF_rtf_paste)) {
+        wchar_t unitab[256];
+        strbuf *rtf = strbuf_new();
+        unsigned char *tdata = (unsigned char *)lock2;
+        wchar_t *udata = (wchar_t *)lock;
+        int uindex = 0, tindex = 0;
+        int multilen, blen, alen, i;
+        char before[16], after[4];
+        int fgcolour,  lastfgcolour  = -1;
+        int bgcolour,  lastbgcolour  = -1;
+        COLORREF fg,   lastfg = -1;
+        COLORREF bg,   lastbg = -1;
+        int attrBold,  lastAttrBold  = 0;
+        int attrUnder, lastAttrUnder = 0;
+        int palette[OSC4_NCOLOURS];
+        int numcolours;
+        tree234 *rgbtree = NULL;
+        FontSpec *font = conf_get_fontspec(conf, CONF_font);
+
+        get_unitab(CP_ACP, unitab, 0);
+
+        put_fmt(
+            rtf, "{\\rtf1\\ansi\\deff0{\\fonttbl\\f0\\fmodern %s;}\\f0\\fs%d",
+            font->name, font->height*2);
+
+        /*
+         * Add colour palette
+         * {\colortbl ;\red255\green0\blue0;\red0\green0\blue128;}
+         */
+
+        /*
+         * First - Determine all colours in use
+         *    o  Foregound and background colours share the same palette
+         */
+        if (attr) {
+            memset(palette, 0, sizeof(palette));
+            for (i = 0; i < (len-1); i++) {
+                fgcolour = ((attr[i] & ATTR_FGMASK) >> ATTR_FGSHIFT);
+                bgcolour = ((attr[i] & ATTR_BGMASK) >> ATTR_BGSHIFT);
+
+                if (attr[i] & ATTR_REVERSE) {
+                    int tmpcolour = fgcolour;   /* Swap foreground and background */
+                    fgcolour = bgcolour;
+                    bgcolour = tmpcolour;
+                }
+
+                if (bold_colours && (attr[i] & ATTR_BOLD)) {
+                    if (fgcolour  <   8)        /* ANSI colours */
+                        fgcolour +=   8;
+                    else if (fgcolour >= 256)   /* Default colours */
+                        fgcolour ++;
+                }
+
+                if ((attr[i] & ATTR_BLINK)) {
+                    if (bgcolour  <   8)        /* ANSI colours */
+                        bgcolour +=   8;
+                    else if (bgcolour >= 256)   /* Default colours */
+                        bgcolour ++;
+                }
+
+                palette[fgcolour]++;
+                palette[bgcolour]++;
+            }
+
+            if (truecolour) {
+                rgbtree = newtree234(cmpCOLORREF);
+                for (i = 0; i < (len-1); i++) {
+                    if (truecolour[i].fg.enabled) {
+                        rgbindex *rgbp = snew(rgbindex);
+                        rgbp->ref = RGB(truecolour[i].fg.r,
+                                        truecolour[i].fg.g,
+                                        truecolour[i].fg.b);
+                        if (add234(rgbtree, rgbp) != rgbp)
+                            sfree(rgbp);
+                    }
+                    if (truecolour[i].bg.enabled) {
+                        rgbindex *rgbp = snew(rgbindex);
+                        rgbp->ref = RGB(truecolour[i].bg.r,
+                                        truecolour[i].bg.g,
+                                        truecolour[i].bg.b);
+                        if (add234(rgbtree, rgbp) != rgbp)
+                            sfree(rgbp);
+                    }
+                }
+            }
+
+            /*
+             * Next - Create a reduced palette
+             */
+            numcolours = 0;
+            for (i = 0; i < OSC4_NCOLOURS; i++) {
+                if (palette[i] != 0)
+                    palette[i]  = ++numcolours;
+            }
+
+            if (rgbtree) {
+                rgbindex *rgbp;
+                for (i = 0; (rgbp = index234(rgbtree, i)) != NULL; i++)
+                    rgbp->index = ++numcolours;
+            }
+
+            /*
+             * Finally - Write the colour table
+             */
+            put_datapl(rtf, PTRLEN_LITERAL("{\\colortbl ;"));
+
+            for (i = 0; i < OSC4_NCOLOURS; i++) {
+                if (palette[i] != 0) {
+                    const PALETTEENTRY *pe = &logpal->palPalEntry[i];
+                    put_fmt(rtf, "\\red%d\\green%d\\blue%d;",
+                            pe->peRed, pe->peGreen, pe->peBlue);
+                }
+            }
+            if (rgbtree) {
+                rgbindex *rgbp;
+                for (i = 0; (rgbp = index234(rgbtree, i)) != NULL; i++)
+                    put_fmt(rtf, "\\red%d\\green%d\\blue%d;",
+                            GetRValue(rgbp->ref), GetGValue(rgbp->ref),
+                            GetBValue(rgbp->ref));
+            }
+            put_datapl(rtf, PTRLEN_LITERAL("}"));
+        }
+
+        /*
+         * We want to construct a piece of RTF that specifies the
+         * same Unicode text. To do this we will read back in
+         * parallel from the Unicode data in `udata' and the
+         * non-Unicode data in `tdata'. For each character in
+         * `tdata' which becomes the right thing in `udata' when
+         * looked up in `unitab', we just copy straight over from
+         * tdata. For each one that doesn't, we must WCToMB it
+         * individually and produce a \u escape sequence.
+         *
+         * It would probably be more robust to just bite the bullet
+         * and WCToMB each individual Unicode character one by one,
+         * then MBToWC each one back to see if it was an accurate
+         * translation; but that strikes me as a horrifying number
+         * of Windows API calls so I want to see if this faster way
+         * will work. If it screws up badly we can always revert to
+         * the simple and slow way.
+         */
+        while (tindex < len2 && uindex < len &&
+               tdata[tindex] && udata[uindex]) {
+            if (tindex + 1 < len2 &&
+                tdata[tindex] == '\r' &&
+                tdata[tindex+1] == '\n') {
+                tindex++;
+                uindex++;
+            }
+
+            /*
+             * Set text attributes
+             */
+            if (attr) {
+                /*
+                 * Determine foreground and background colours
+                 */
+                if (truecolour && truecolour[tindex].fg.enabled) {
+                    fgcolour = -1;
+                    fg = RGB(truecolour[tindex].fg.r,
+                             truecolour[tindex].fg.g,
+                             truecolour[tindex].fg.b);
+                } else {
+                    fgcolour = ((attr[tindex] & ATTR_FGMASK) >> ATTR_FGSHIFT);
+                    fg = -1;
+                }
+
+                if (truecolour && truecolour[tindex].bg.enabled) {
+                    bgcolour = -1;
+                    bg = RGB(truecolour[tindex].bg.r,
+                             truecolour[tindex].bg.g,
+                             truecolour[tindex].bg.b);
+                } else {
+                    bgcolour = ((attr[tindex] & ATTR_BGMASK) >> ATTR_BGSHIFT);
+                    bg = -1;
+                }
+
+                if (attr[tindex] & ATTR_REVERSE) {
+                    int tmpcolour = fgcolour;       /* Swap foreground and background */
+                    fgcolour = bgcolour;
+                    bgcolour = tmpcolour;
+
+                    COLORREF tmpref = fg;
+                    fg = bg;
+                    bg = tmpref;
+                }
+
+                if (bold_colours && (attr[tindex] & ATTR_BOLD) && (fgcolour >= 0)) {
+                    if (fgcolour  <   8)            /* ANSI colours */
+                        fgcolour +=   8;
+                    else if (fgcolour >= 256)       /* Default colours */
+                        fgcolour ++;
+                }
+
+                if ((attr[tindex] & ATTR_BLINK) && (bgcolour >= 0)) {
+                    if (bgcolour  <   8)            /* ANSI colours */
+                        bgcolour +=   8;
+                    else if (bgcolour >= 256)       /* Default colours */
+                        bgcolour ++;
+                }
+
+                /*
+                 * Collect other attributes
+                 */
+                if (bold_font_mode != BOLD_NONE)
+                    attrBold  = attr[tindex] & ATTR_BOLD;
+                else
+                    attrBold  = 0;
+
+                attrUnder = attr[tindex] & ATTR_UNDER;
+
+                /*
+                 * Reverse video
+                 *   o  If video isn't reversed, ignore colour attributes for default foregound
+                 *      or background.
+                 *   o  Special case where bolded text is displayed using the default foregound
+                 *      and background colours - force to bolded RTF.
+                 */
+                if (!(attr[tindex] & ATTR_REVERSE)) {
+                    if (bgcolour >= 256)            /* Default color */
+                        bgcolour  = -1;             /* No coloring */
+
+                    if (fgcolour >= 256) {          /* Default colour */
+                        if (bold_colours && (fgcolour & 1) && bgcolour == -1)
+                            attrBold = ATTR_BOLD;   /* Emphasize text with bold attribute */
+
+                        fgcolour  = -1;             /* No coloring */
+                    }
+                }
+
+                /*
+                 * Write RTF text attributes
+                 */
+                if ((lastfgcolour != fgcolour) || (lastfg != fg)) {
+                    lastfgcolour  = fgcolour;
+                    lastfg        = fg;
+                    if (fg == -1) {
+                        put_fmt(rtf, "\\cf%d ",
+                                (fgcolour >= 0) ? palette[fgcolour] : 0);
+                    } else {
+                        rgbindex rgb, *rgbp;
+                        rgb.ref = fg;
+                        if ((rgbp = find234(rgbtree, &rgb, NULL)) != NULL)
+                            put_fmt(rtf, "\\cf%d ", rgbp->index);
+                    }
+                }
+
+                if ((lastbgcolour != bgcolour) || (lastbg != bg)) {
+                    lastbgcolour  = bgcolour;
+                    lastbg        = bg;
+                    if (bg == -1)
+                        put_fmt(rtf, "\\highlight%d ",
+                                (bgcolour >= 0) ? palette[bgcolour] : 0);
+                    else {
+                        rgbindex rgb, *rgbp;
+                        rgb.ref = bg;
+                        if ((rgbp = find234(rgbtree, &rgb, NULL)) != NULL)
+                            put_fmt(rtf, "\\highlight%d ", rgbp->index);
+                    }
+                }
+
+                if (lastAttrBold != attrBold) {
+                    lastAttrBold  = attrBold;
+                    put_datapl(rtf, attrBold ?
+                               PTRLEN_LITERAL("\\b ") :
+                               PTRLEN_LITERAL("\\b0 "));
+                }
+
+                if (lastAttrUnder != attrUnder) {
+                    lastAttrUnder  = attrUnder;
+                    put_datapl(rtf, attrUnder ?
+                               PTRLEN_LITERAL("\\ul ") :
+                               PTRLEN_LITERAL("\\ulnone "));
+                }
+            }
+
+            if (unitab[tdata[tindex]] == udata[uindex]) {
+                multilen = 1;
+                before[0] = '\0';
+                after[0] = '\0';
+                blen = alen = 0;
+            } else {
+                multilen = WideCharToMultiByte(CP_ACP, 0, unitab+uindex, 1,
+                                               NULL, 0, NULL, NULL);
+                if (multilen != 1) {
+                    blen = sprintf(before, "{\\uc%d\\u%d", (int)multilen,
+                                   (int)udata[uindex]);
+                    alen = 1; strcpy(after, "}");
+                } else {
+                    blen = sprintf(before, "\\u%d", (int)udata[uindex]);
+                    alen = 0; after[0] = '\0';
+                }
+            }
+            assert(tindex + multilen <= len2);
+
+            put_data(rtf, before, blen);
+            for (i = 0; i < multilen; i++) {
+                if (tdata[tindex+i] == '\\' ||
+                    tdata[tindex+i] == '{' ||
+                    tdata[tindex+i] == '}') {
+                    put_byte(rtf, '\\');
+                    put_byte(rtf, tdata[tindex+i]);
+                } else if (tdata[tindex+i] == 0x0D || tdata[tindex+i] == 0x0A) {
+                    put_datapl(rtf, PTRLEN_LITERAL("\\par\r\n"));
+                } else if (tdata[tindex+i] > 0x7E || tdata[tindex+i] < 0x20) {
+                    put_fmt(rtf, "\\'%02x", tdata[tindex+i]);
+                } else {
+                    put_byte(rtf, tdata[tindex+i]);
+                }
+            }
+            put_data(rtf, after, alen);
+
+            tindex += multilen;
+            uindex++;
+        }
+
+        put_datapl(rtf, PTRLEN_LITERAL("}\0\0")); /* Terminate RTF stream */
+
+        clipdata3 = GlobalAlloc(GMEM_DDESHARE | GMEM_MOVEABLE, rtf->len);
+        if (clipdata3 && (lock3 = GlobalLock(clipdata3)) != NULL) {
+            memcpy(lock3, rtf->u, rtf->len);
+            GlobalUnlock(clipdata3);
+        }
+        strbuf_free(rtf);
+
+        if (rgbtree) {
+            rgbindex *rgbp;
+            while ((rgbp = delpos234(rgbtree, 0)) != NULL)
+                sfree(rgbp);
+            freetree234(rgbtree);
+        }
+    } else
+        clipdata3 = NULL;
+
+    GlobalUnlock(clipdata);
+    GlobalUnlock(clipdata2);
+
+    if (!must_deselect)
+        SendMessage(wgs.term_hwnd, WM_IGNORE_CLIP, true, 0);
+
+    if (OpenClipboard(wgs.term_hwnd)) {
+        EmptyClipboard();
+        SetClipboardData(CF_UNICODETEXT, clipdata);
+        SetClipboardData(CF_TEXT, clipdata2);
+        if (clipdata3)
+            SetClipboardData(RegisterClipboardFormat(CF_RTF), clipdata3);
+        CloseClipboard();
+    } else {
+        GlobalFree(clipdata);
+        GlobalFree(clipdata2);
+    }
+
+    if (!must_deselect)
+        SendMessage(wgs.term_hwnd, WM_IGNORE_CLIP, false, 0);
+}
+
+static DWORD WINAPI clipboard_read_threadfunc(void *param)
+{
+    HWND hwnd = (HWND)param;
+    HGLOBAL clipdata;
+
+    if (OpenClipboard(NULL)) {
+        if ((clipdata = GetClipboardData(CF_UNICODETEXT))) {
+            SendMessage(hwnd, WM_GOT_CLIPDATA,
+                        (WPARAM)true, (LPARAM)clipdata);
+        } else if ((clipdata = GetClipboardData(CF_TEXT))) {
+            SendMessage(hwnd, WM_GOT_CLIPDATA,
+                        (WPARAM)false, (LPARAM)clipdata);
+        }
+        CloseClipboard();
+    }
+
+    return 0;
+}
+
+static void process_clipdata(HGLOBAL clipdata, bool unicode)
+{
+    wchar_t *clipboard_contents = NULL;
+    size_t clipboard_length = 0;
+
+    if (unicode) {
+        wchar_t *p = GlobalLock(clipdata);
+        wchar_t *p2;
+
+        if (p) {
+            /* Unwilling to rely on Windows having wcslen() */
+            for (p2 = p; *p2; p2++);
+            clipboard_length = p2 - p;
+            clipboard_contents = snewn(clipboard_length + 1, wchar_t);
+            memcpy(clipboard_contents, p, clipboard_length * sizeof(wchar_t));
+            clipboard_contents[clipboard_length] = L'\0';
+            term_do_paste(term, clipboard_contents, clipboard_length);
+        }
+    } else {
+        char *s = GlobalLock(clipdata);
+        int i;
+
+        if (s) {
+            i = MultiByteToWideChar(CP_ACP, 0, s, strlen(s) + 1, 0, 0);
+            clipboard_contents = snewn(i, wchar_t);
+            MultiByteToWideChar(CP_ACP, 0, s, strlen(s) + 1,
+                                clipboard_contents, i);
+            clipboard_length = i - 1;
+            clipboard_contents[clipboard_length] = L'\0';
+            term_do_paste(term, clipboard_contents, clipboard_length);
+        }
+    }
+
+    sfree(clipboard_contents);
+}
+
+static void wintw_clip_request_paste(TermWin *tw, int clipboard)
+{
+    assert(clipboard == CLIP_SYSTEM);
+
+    /*
+     * I always thought pasting was synchronous in Windows; the
+     * clipboard access functions certainly _look_ synchronous,
+     * unlike the X ones. But in fact it seems that in some
+     * situations the contents of the clipboard might not be
+     * immediately available, and the clipboard-reading functions
+     * may block. This leads to trouble if the application
+     * delivering the clipboard data has to get hold of it by -
+     * for example - talking over a network connection which is
+     * forwarded through this very PuTTY.
+     *
+     * Hence, we spawn a subthread to read the clipboard, and do
+     * our paste when it's finished. The thread will send a
+     * message back to our main window when it terminates, and
+     * that tells us it's OK to paste.
+     */
+    DWORD in_threadid; /* required for Win9x */
+    HANDLE hThread = CreateThread(NULL, 0, clipboard_read_threadfunc,
+                                  wgs.term_hwnd, 0, &in_threadid);
+    if (hThread)
+        CloseHandle(hThread);          /* we don't need the thread handle */
+}
+
+/*
+ * Print a modal (Really Bad) message box and perform a fatal exit.
+ */
+void modalfatalbox(const char *fmt, ...)
+{
+    va_list ap;
+    char *message, *title;
+
+    va_start(ap, fmt);
+    message = dupvprintf(fmt, ap);
+    va_end(ap);
+    show_mouseptr(true);
+    title = dupprintf("%s Fatal Error", appname);
+    MessageBox(wgs.term_hwnd, message, title,
+               MB_SYSTEMMODAL | MB_ICONERROR | MB_OK);
+    sfree(message);
+    sfree(title);
+    cleanup_exit(1);
+}
+
+/*
+ * Print a message box and don't close the connection.
+ */
+void nonfatal(const char *fmt, ...)
+{
+    va_list ap;
+    char *message, *title;
+
+    va_start(ap, fmt);
+    message = dupvprintf(fmt, ap);
+    va_end(ap);
+    show_mouseptr(true);
+    title = dupprintf("%s Error", appname);
+    MessageBox(wgs.term_hwnd, message, title, MB_ICONERROR | MB_OK);
+    sfree(message);
+    sfree(title);
+}
+
+static bool flash_window_ex(DWORD dwFlags, UINT uCount, DWORD dwTimeout)
+{
+    if (p_FlashWindowEx) {
+        FLASHWINFO fi;
+        fi.cbSize = sizeof(fi);
+        fi.hwnd = wgs.term_hwnd;
+        fi.dwFlags = dwFlags;
+        fi.uCount = uCount;
+        fi.dwTimeout = dwTimeout;
+        return (*p_FlashWindowEx)(&fi);
+    }
+    else
+        return false; /* shrug */
+}
+
+static void flash_window(int mode);
+static long next_flash;
+static bool flashing = false;
+
+/*
+ * Timer for platforms where we must maintain window flashing manually
+ * (e.g., Win95).
+ */
+static void flash_window_timer(void *ctx, unsigned long now)
+{
+    if (flashing && now == next_flash) {
+        flash_window(1);
+    }
+}
+
+/*
+ * Manage window caption / taskbar flashing, if enabled.
+ * 0 = stop, 1 = maintain, 2 = start
+ */
+static void flash_window(int mode)
+{
+    int beep_ind = conf_get_int(conf, CONF_beep_ind);
+    if ((mode == 0) || (beep_ind == B_IND_DISABLED)) {
+        /* stop */
+        if (flashing) {
+            flashing = false;
+            if (p_FlashWindowEx)
+                flash_window_ex(FLASHW_STOP, 0, 0);
+            else
+                FlashWindow(wgs.term_hwnd, false);
+        }
+
+    } else if (mode == 2) {
+        /* start */
+        if (!flashing) {
+            flashing = true;
+            if (p_FlashWindowEx) {
+                /* For so-called "steady" mode, we use uCount=2, which
+                 * seems to be the traditional number of flashes used
+                 * by user notifications (e.g., by Explorer).
+                 * uCount=0 appears to enable continuous flashing, per
+                 * "flashing" mode, although I haven't seen this
+                 * documented. */
+                flash_window_ex(FLASHW_ALL | FLASHW_TIMER,
+                                (beep_ind == B_IND_FLASH ? 0 : 2),
+                                0 /* system cursor blink rate */);
+                /* No need to schedule timer */
+            } else {
+                FlashWindow(wgs.term_hwnd, true);
+                next_flash = schedule_timer(450, flash_window_timer,
+                                            wgs.term_hwnd);
+            }
+        }
+
+    } else if ((mode == 1) && (beep_ind == B_IND_FLASH)) {
+        /* maintain */
+        if (flashing && !p_FlashWindowEx) {
+            FlashWindow(wgs.term_hwnd, true);    /* toggle */
+            next_flash = schedule_timer(450, flash_window_timer,
+                                        wgs.term_hwnd);
+        }
+    }
+}
+
+/*
+ * Beep.
+ */
+static void wintw_bell(TermWin *tw, int mode)
+{
+    if (mode == BELL_DEFAULT) {
+        /*
+         * For MessageBeep style bells, we want to be careful of
+         * timing, because they don't have the nice property of
+         * PlaySound bells that each one cancels the previous
+         * active one. So we limit the rate to one per 50ms or so.
+         */
+        static long lastbeep = 0;
+        long beepdiff;
+
+        beepdiff = GetTickCount() - lastbeep;
+        if (beepdiff >= 0 && beepdiff < 50)
+            return;
+        MessageBeep(MB_OK);
+        /*
+         * The above MessageBeep call takes time, so we record the
+         * time _after_ it finishes rather than before it starts.
+         */
+        lastbeep = GetTickCount();
+    } else if (mode == BELL_WAVEFILE) {
+        Filename *bell_wavefile = conf_get_filename(conf, CONF_bell_wavefile);
+        if (!p_PlaySound || !p_PlaySound(bell_wavefile->path, NULL,
+                                         SND_ASYNC | SND_FILENAME)) {
+            char *buf, *otherbuf;
+            show_mouseptr(true);
+            buf = dupprintf(
+                "Unable to play sound file\n%s\nUsing default sound instead",
+                bell_wavefile->path);
+            otherbuf = dupprintf("%s Sound Error", appname);
+            MessageBox(wgs.term_hwnd, buf, otherbuf,
+                       MB_OK | MB_ICONEXCLAMATION);
+            sfree(buf);
+            sfree(otherbuf);
+            conf_set_int(conf, CONF_beep, BELL_DEFAULT);
+        }
+    } else if (mode == BELL_PCSPEAKER) {
+        static long lastbeep = 0;
+        long beepdiff;
+
+        beepdiff = GetTickCount() - lastbeep;
+        if (beepdiff >= 0 && beepdiff < 50)
+            return;
+
+        /*
+         * We must beep in different ways depending on whether this
+         * is a 95-series or NT-series OS.
+         */
+        if (osPlatformId == VER_PLATFORM_WIN32_NT)
+            Beep(800, 100);
+        else
+            MessageBeep(-1);
+        lastbeep = GetTickCount();
+    }
+    /* Otherwise, either visual bell or disabled; do nothing here */
+    if (!term->has_focus) {
+        flash_window(2);               /* start */
+    }
+}
+
+/*
+ * Minimise or restore the window in response to a server-side
+ * request.
+ */
+static void wintw_set_minimised(TermWin *tw, bool minimised)
+{
+    if (IsIconic(wgs.term_hwnd)) {
+        if (!minimised)
+            ShowWindow(wgs.term_hwnd, SW_RESTORE);
+    } else {
+        if (minimised)
+            ShowWindow(wgs.term_hwnd, SW_MINIMIZE);
+    }
+}
+
+/*
+ * Move the window in response to a server-side request.
+ */
+static void wintw_move(TermWin *tw, int x, int y)
+{
+    int resize_action = conf_get_int(conf, CONF_resize_action);
+    if (resize_action == RESIZE_DISABLED ||
+        resize_action == RESIZE_FONT ||
+        IsZoomed(wgs.term_hwnd))
+        return;
+
+    SetWindowPos(wgs.term_hwnd, NULL, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
+}
+
+/*
+ * Move the window to the top or bottom of the z-order in response
+ * to a server-side request.
+ */
+static void wintw_set_zorder(TermWin *tw, bool top)
+{
+    if (conf_get_bool(conf, CONF_alwaysontop))
+        return;                        /* ignore */
+    SetWindowPos(wgs.term_hwnd, top ? HWND_TOP : HWND_BOTTOM, 0, 0, 0, 0,
+                 SWP_NOMOVE | SWP_NOSIZE);
+}
+
+/*
+ * Refresh the window in response to a server-side request.
+ */
+static void wintw_refresh(TermWin *tw)
+{
+    InvalidateRect(wgs.term_hwnd, NULL, true);
+}
+
+/*
+ * Maximise or restore the window in response to a server-side
+ * request.
+ */
+static void wintw_set_maximised(TermWin *tw, bool maximised)
+{
+    if (IsZoomed(wgs.term_hwnd)) {
+        if (!maximised)
+            ShowWindow(wgs.term_hwnd, SW_RESTORE);
+    } else {
+        if (maximised)
+            ShowWindow(wgs.term_hwnd, SW_MAXIMIZE);
+    }
+}
+
+/*
+ * See if we're in full-screen mode.
+ */
+static bool is_full_screen()
+{
+    if (!IsZoomed(wgs.term_hwnd))
+        return false;
+    if (GetWindowLongPtr(wgs.term_hwnd, GWL_STYLE) & WS_CAPTION)
+        return false;
+    return true;
+}
+
+/* Get the rect/size of a full screen window using the nearest available
+ * monitor in multimon systems; default to something sensible if only
+ * one monitor is present. */
+static bool get_fullscreen_rect(RECT *ss)
+{
+#if defined(MONITOR_DEFAULTTONEAREST) && !defined(NO_MULTIMON)
+    if (p_GetMonitorInfoA && p_MonitorFromWindow) {
+        HMONITOR mon;
+        MONITORINFO mi;
+        mon = p_MonitorFromWindow(wgs.term_hwnd, MONITOR_DEFAULTTONEAREST);
+        mi.cbSize = sizeof(mi);
+        p_GetMonitorInfoA(mon, &mi);
+
+        /* structure copy */
+        *ss = mi.rcMonitor;
+        return true;
+    }
+#endif
+/* could also use code like this:
+        ss->left = ss->top = 0;
+        ss->right = GetSystemMetrics(SM_CXSCREEN);
+        ss->bottom = GetSystemMetrics(SM_CYSCREEN);
+*/
+    return GetClientRect(GetDesktopWindow(), ss);
+}
+
+
+/*
+ * Go full-screen. This should only be called when we are already
+ * maximised.
+ */
+static void make_full_screen()
+{
+    DWORD style;
+    RECT ss;
+
+    assert(IsZoomed(wgs.term_hwnd));
+
+    if (is_full_screen())
+        return;
+
+    /* Remove the window furniture. */
+    style = GetWindowLongPtr(wgs.term_hwnd, GWL_STYLE);
+    style &= ~(WS_CAPTION | WS_BORDER | WS_THICKFRAME);
+    if (conf_get_bool(conf, CONF_scrollbar_in_fullscreen))
+        style |= WS_VSCROLL;
+    else
+        style &= ~WS_VSCROLL;
+    SetWindowLongPtr(wgs.term_hwnd, GWL_STYLE, style);
+
+    /* Resize ourselves to exactly cover the nearest monitor. */
+    get_fullscreen_rect(&ss);
+    SetWindowPos(wgs.term_hwnd, HWND_TOP, ss.left, ss.top,
+                 ss.right - ss.left, ss.bottom - ss.top, SWP_FRAMECHANGED);
+
+    /* We may have changed size as a result */
+
+    reset_window(0);
+
+    /* Tick the menu item in the System and context menus. */
+    {
+        int i;
+        for (i = 0; i < lenof(popup_menus); i++)
+            CheckMenuItem(popup_menus[i].menu, IDM_FULLSCREEN, MF_CHECKED);
+    }
+}
+
+/*
+ * Clear the full-screen attributes.
+ */
+static void clear_full_screen()
+{
+    DWORD oldstyle, style;
+
+    /* Reinstate the window furniture. */
+    style = oldstyle = GetWindowLongPtr(wgs.term_hwnd, GWL_STYLE);
+    style |= WS_CAPTION | WS_BORDER;
+    if (conf_get_int(conf, CONF_resize_action) == RESIZE_DISABLED)
+        style &= ~WS_THICKFRAME;
+    else
+        style |= WS_THICKFRAME;
+    if (conf_get_bool(conf, CONF_scrollbar))
+        style |= WS_VSCROLL;
+    else
+        style &= ~WS_VSCROLL;
+    if (style != oldstyle) {
+        SetWindowLongPtr(wgs.term_hwnd, GWL_STYLE, style);
+        SetWindowPos(wgs.term_hwnd, NULL, 0, 0, 0, 0,
+                     SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
+                     SWP_FRAMECHANGED);
+    }
+
+    /* Untick the menu item in the System and context menus. */
+    {
+        int i;
+        for (i = 0; i < lenof(popup_menus); i++)
+            CheckMenuItem(popup_menus[i].menu, IDM_FULLSCREEN, MF_UNCHECKED);
+    }
+}
+
+/*
+ * Toggle full-screen mode.
+ */
+static void flip_full_screen()
+{
+    if (is_full_screen()) {
+        ShowWindow(wgs.term_hwnd, SW_RESTORE);
+    } else if (IsZoomed(wgs.term_hwnd)) {
+        make_full_screen();
+    } else {
+        SendMessage(wgs.term_hwnd, WM_FULLSCR_ON_MAX, 0, 0);
+        ShowWindow(wgs.term_hwnd, SW_MAXIMIZE);
+    }
+}
+
+static size_t win_seat_output(Seat *seat, SeatOutputType type,
+                              const void *data, size_t len)
+{
+    return term_data(term, data, len);
+}
+
+static void wintw_unthrottle(TermWin *win, size_t bufsize)
+{
+    if (backend)
+        backend_unthrottle(backend, bufsize);
+}
+
+static bool win_seat_eof(Seat *seat)
+{
+    return true;   /* do respond to incoming EOF with outgoing */
+}
+
+static SeatPromptResult win_seat_get_userpass_input(Seat *seat, prompts_t *p)
+{
+    SeatPromptResult spr;
+    spr = cmdline_get_passwd_input(p, &cmdline_get_passwd_state, true);
+    if (spr.kind == SPRK_INCOMPLETE)
+        spr = term_get_userpass_input(term, p);
+    return spr;
+}
+
+static void win_seat_set_trust_status(Seat *seat, bool trusted)
+{
+    term_set_trust_status(term, trusted);
+}
+
+static bool win_seat_can_set_trust_status(Seat *seat)
+{
+    return true;
+}
+
+static bool win_seat_get_cursor_position(Seat *seat, int *x, int *y)
+{
+    term_get_cursor_position(term, x, y);
+    return true;
+}
+
+static bool win_seat_get_window_pixel_size(Seat *seat, int *x, int *y)
+{
+    RECT r;
+    GetWindowRect(wgs.term_hwnd, &r);
+    *x = r.right - r.left;
+    *y = r.bottom - r.top;
+    return true;
+}
