1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-09 01:18:00 +00:00
putty-source/terminal/lineedit.c
Simon Tatham 363debc7f0 lineedit: make both ^M and ^J terminate a line.
In protocols other than PROT_RAW, the new line editing system differed
from the old one in not considering ^M or ^J (typed using the actual
Ctrl key, so distinct from pressing Return) to mean "I've finished
editing this line, please send it". This commit reinstates that
behaviour.

It turned out that a third-party tool (namely PuTTY Connection Manager),
which automatically answers prompts for the user, was terminating them
by sending ^J in place of the Return key. We don't know why (and it's
now unmaintained), but it was. So this change should make that tool
start working again.

I exclude PROT_RAW above because in that protocol the line editing has
much weirder handling for ^M and ^J, which lineedit replicated
faithfully from the old code: either control character by itself is
treated literally (displaying as "^M" or "^J" in the terminal), but if
you type the two in sequence in that order, then the ^J deletes the ^M
from the edit buffer and enters the line, so that the sequence CR LF
acts as a newline overall. I haven't changed that behaviour here, but
I have added a regression test of it to test_lineedit.
2024-12-15 19:23:21 +00:00

550 lines
17 KiB
C

/*
* Implementation of local line editing. Used during username and
* password input at login time, and also by ldisc during the main
* session (if the session's virtual terminal is in that mode).
*
* Because we're tied into a real GUI terminal (and not a completely
* standalone line-discipline module that deals purely with byte
* streams), we can support a slightly richer input interface than
* plain bytes.
*
* In particular, the 'dedicated' flag sent along with every byte is
* used to distinguish control codes input via Ctrl+letter from the
* same code input by a dedicated key like Return or Backspace. This
* allows us to interpret the Ctrl+letter key combination as inputting
* a literal control character to go into the line buffer, and the
* dedicated-key version as performing an editing function.
*/
#include "putty.h"
#include "terminal.h"
typedef struct BufChar BufChar;
struct TermLineEditor {
Terminal *term;
BufChar *head, *tail;
unsigned flags;
bool quote_next_char;
TermLineEditorCallbackReceiver *receiver;
};
struct BufChar {
BufChar *prev, *next;
/* The bytes of the character, to be sent on the wire */
char wire[6];
uint8_t nwire;
/* Whether this character is considered complete */
bool complete;
/* Width of the character when it was displayed, in terminal cells */
uint8_t width;
/* Whether this character counts as whitespace, for ^W purposes */
bool space;
};
TermLineEditor *lineedit_new(Terminal *term, unsigned flags,
TermLineEditorCallbackReceiver *receiver)
{
TermLineEditor *le = snew(TermLineEditor);
le->term = term;
le->head = le->tail = NULL;
le->flags = flags;
le->quote_next_char = false;
le->receiver = receiver;
return le;
}
static void bufchar_free(BufChar *bc)
{
smemclr(bc, sizeof(*bc));
sfree(bc);
}
static void lineedit_free_buffer(TermLineEditor *le)
{
while (le->head) {
BufChar *bc = le->head;
le->head = bc->next;
bufchar_free(bc);
}
le->tail = NULL;
}
void lineedit_free(TermLineEditor *le)
{
lineedit_free_buffer(le);
sfree(le);
}
void lineedit_modify_flags(TermLineEditor *le, unsigned clr, unsigned flip)
{
le->flags &= ~clr;
le->flags ^= flip;
}
static void lineedit_term_write(TermLineEditor *le, ptrlen data)
{
le->receiver->vt->to_terminal(le->receiver, data);
}
static void lineedit_term_newline(TermLineEditor *le)
{
lineedit_term_write(le, PTRLEN_LITERAL("\x0D\x0A"));
}
static inline void lineedit_send_data(TermLineEditor *le, ptrlen data)
{
le->receiver->vt->to_backend(le->receiver, data);
}
static inline void lineedit_special(TermLineEditor *le,
SessionSpecialCode code, int arg)
{
le->receiver->vt->special(le->receiver, code, arg);
}
static inline void lineedit_send_newline(TermLineEditor *le)
{
le->receiver->vt->newline(le->receiver);
}
static void lineedit_delete_char(TermLineEditor *le)
{
if (le->tail) {
BufChar *bc = le->tail;
le->tail = bc->prev;
if (!le->tail)
le->head = NULL;
else
le->tail->next = NULL;
for (unsigned i = 0; i < bc->width; i++)
lineedit_term_write(le, PTRLEN_LITERAL("\x08 \x08"));
bufchar_free(bc);
}
}
static void lineedit_delete_word(TermLineEditor *le)
{
/*
* Deleting a word stops at the _start_ of a word, i.e. at any
* boundary with a space on the left and a non-space on the right.
*/
if (!le->tail)
return;
while (true) {
bool deleted_char_is_space = le->tail->space;
lineedit_delete_char(le);
if (!le->tail)
break; /* we've cleared the whole line */
if (le->tail->space && !deleted_char_is_space)
break; /* we've just reached a word boundary */
}
}
static void lineedit_delete_line(TermLineEditor *le)
{
while (le->tail)
lineedit_delete_char(le);
lineedit_special(le, SS_EL, 0);
}
void lineedit_send_line(TermLineEditor *le)
{
bufchain output;
bufchain_init(&output);
for (BufChar *bc = le->head; bc; bc = bc->next)
bufchain_add(&output, bc->wire, bc->nwire);
while (bufchain_size(&output) > 0) {
ptrlen data = bufchain_prefix(&output);
lineedit_send_data(le, data);
bufchain_consume(&output, data.len);
}
bufchain_clear(&output);
lineedit_free_buffer(le);
le->quote_next_char = false;
}
static void lineedit_complete_line(TermLineEditor *le)
{
lineedit_term_newline(le);
lineedit_send_line(le);
lineedit_send_newline(le);
}
/*
* Send data to the terminal to display a BufChar. As a side effect,
* update bc->width to indicate how many character cells we think were
* taken up by what we just wrote. No other change to bc is made.
*/
static void lineedit_display_bufchar(TermLineEditor *le, BufChar *bc,
unsigned chr)
{
char buf[6];
buffer_sink bs[1];
buffer_sink_init(bs, buf, lenof(buf));
/* Handle single-byte character set translation. */
if (!in_utf(le->term) && DIRECT_CHAR(chr)) {
/*
* If we're not in UTF-8, i.e. we're in a single-byte
* character set, then first we must feed the input byte
* through term_translate, which will tell us whether it's a
* control character or not. (That varies with the charset:
* e.g. ISO 8859-1 and Win1252 disagree on a lot of
* 0x80-0x9F).
*
* In principle, we could pass NULL as our term_utf8_decode
* pointer, on the grounds that since the terminal isn't in
* UTF-8 mode term_translate shouldn't access it. But that
* seems needlessly reckless; we'll make up an empty one.
*/
term_utf8_decode dummy_utf8 = { .state = 0, .chr = 0, .size = 0 };
chr = term_translate(
le->term, &dummy_utf8, (unsigned char)chr);
/*
* After that, chr will be either a control-character value
* (00-1F, 7F, 80-9F), or a byte value ORed with one of the
* CSET_FOO character set indicators. The latter indicates
* that it's a printing character in this charset, in which
* case it takes up one character cell.
*/
if (chr & CSET_MASK) {
put_byte(bs, chr);
bc->width = 1;
goto got_char;
}
}
/*
* If we got here without taking the 'goto' above, then we're now
* holding an actual Unicode character.
*/
assert(!IS_SURROGATE(chr)); /* and it should be an _actual_ one */
/*
* Deal with symbolic representations of control characters.
*/
if (chr < 0x20 || chr == 0x7F) {
/*
* Represent C0 controls as '^C' or similar, and 7F as ^?.
*/
put_byte(bs, '^');
put_byte(bs, chr ^ 0x40);
bc->width = 2;
goto got_char;
}
if (chr >= 0x80 && chr < 0xA0) {
/*
* Represent C1 controls as <9B> or similar.
*/
put_fmt(bs, "<%02X>", chr);
bc->width = 4;
goto got_char;
}
/*
* And if we get _here_, we're holding a printing (or at least not
* _control_, even if zero-width) Unicode character, which _must_
* mean that the terminal is currently in UTF-8 mode (since if it
* were not then printing characters would have gone through the
* term_translate case above). So we can just write the UTF-8 for
* the character - but we must also pay attention to its width in
* character cells, which might be 0, 1 or 2.
*/
assert(in_utf(le->term));
put_utf8_char(bs, chr);
bc->width = term_char_width(le->term, chr);
got_char:
lineedit_term_write(le, make_ptrlen(buf, bs->out - buf));
}
/* Called when we've just added a byte to a UTF-8 character and want
* to see if it's complete */
static void lineedit_check_utf8_complete(TermLineEditor *le, BufChar *bc)
{
BinarySource src[1];
BinarySource_BARE_INIT(src, bc->wire, bc->nwire);
DecodeUTF8Failure err;
unsigned chr = decode_utf8(src, &err);
if (err == DUTF8_E_OUT_OF_DATA)
return; /* not complete yet */
/* Any other error code is regarded as complete, and we just
* display the character as the U+FFFD that decode_utf8 will have
* returned anyway */
bc->complete = true;
bc->space = (chr == ' ');
lineedit_display_bufchar(le, bc, chr);
}
static void lineedit_input_printing_char(TermLineEditor *le, char ch);
static void lineedit_redraw_line(TermLineEditor *le)
{
/* FIXME: I'm not 100% sure this is the behaviour I really want in
* this situation, but it's at least very simple to implement */
BufChar *prevhead = le->head;
le->head = le->tail = NULL;
while (prevhead) {
BufChar *bc = prevhead;
prevhead = prevhead->next;
for (unsigned i = 0; i < bc->nwire; i++)
lineedit_input_printing_char(le, bc->wire[i]);
bufchar_free(bc);
}
}
#define CTRL(c) ((char) (0x40 ^ (unsigned char)c))
void lineedit_input(TermLineEditor *le, char ch, bool dedicated)
{
if (le->quote_next_char) {
/*
* If the previous keypress was ^V, 'quoting' the next
* character to be treated literally, then skip all the
* editing-control processing, and clear that flag.
*/
le->quote_next_char = false;
} else {
/*
* Input events that are only valid with the 'dedicated' flag.
* These are limited to the control codes that _have_
* dedicated keys.
*
* Any case we actually handle here ends with a 'return'
* statement, so that if we fall out of the end of this switch
* at all, it's because the byte hasn't been handled here and
* will fall into the next switch dealing with ordinary input.
*/
if (dedicated) {
switch (ch) {
/*
* The Backspace key.
*
* Since our terminal can be configured to send either
* ^H or 7F (aka ^?) via the backspace key, we accept
* both.
*
* (We could query the Terminal's configuration here
* and accept only the one of those codes that the
* terminal is currently set to. But it's pointless,
* because whichever one the terminal isn't set to,
* the front end won't be sending it with
* dedicated=true anyway.)
*/
case CTRL('H'):
case 0x7F:
lineedit_delete_char(le);
return;
/*
* The Return key.
*/
case CTRL('M'):
lineedit_complete_line(le);
return;
}
}
/*
* Editing and special functions in response to ordinary keys
* or Ctrl+key combinations. Many editing functions have to be
* supported in this mode, like ^W and ^U, because there are
* no dedicated keys that generate the same control codes
* anyway.
*
* Again, we return if the key was handled. The final
* processing of ordinary data to go into the input buffer
* happens if we break from this switch.
*/
switch (ch) {
case CTRL('W'):
lineedit_delete_word(le);
return;
case CTRL('U'):
lineedit_delete_line(le);
return;
case CTRL('['):
if (!(le->flags & LE_ESC_ERASES))
break; /* treat as normal input */
lineedit_delete_line(le);
return;
case CTRL('R'):
lineedit_term_write(le, PTRLEN_LITERAL("^R"));
lineedit_term_newline(le);
lineedit_redraw_line(le);
return;
case CTRL('V'):
le->quote_next_char = true;
return;
case CTRL('C'):
lineedit_delete_line(le);
if (!(le->flags & LE_INTERRUPT))
break; /* treat as normal input */
lineedit_special(le, SS_IP, 0);
return;
case CTRL('Z'):
lineedit_delete_line(le);
if (!(le->flags & LE_SUSPEND))
break; /* treat as normal input */
lineedit_special(le, SS_SUSP, 0);
return;
case CTRL('\\'):
lineedit_delete_line(le);
if (!(le->flags & LE_ABORT))
break; /* treat as normal input */
lineedit_special(le, SS_ABORT, 0);
return;
case CTRL('D'):
if (le->flags & LE_EOF_ALWAYS) {
/* Our client wants to treat ^D / EOF as a special
* character in their own way. Just send an EOF
* special. */
lineedit_special(le, SS_EOF, 0);
return;
}
/*
* Otherwise, ^D has the same behaviour as in Unix tty
* line editing: if the edit buffer is non-empty then it's
* sent immediately without a newline, and if it is empty
* then an EOF is sent.
*/
if (le->head) {
lineedit_send_line(le);
return;
}
lineedit_special(le, SS_EOF, 0);
return;
case CTRL('J'):
if (le->flags & LE_CRLF_NEWLINE) {
/*
* If the previous character in the buffer is a
* literal Ctrl-M, and now the user sends Ctrl-J, then
* we amalgamate both into a newline event.
*/
if (le->tail && le->tail->nwire == 1 &&
le->tail->wire[0] == CTRL('M')) {
lineedit_delete_char(le); /* erase ^J from buffer */
lineedit_complete_line(le);
return;
}
} else {
/* If we're not in LE_CRLF_NEWLINE mode, then ^J by
* itself acts as a full newline character */
lineedit_complete_line(le);
return;
}
case CTRL('M'):
if (le->flags & LE_CRLF_NEWLINE) {
/* In this mode, ^M is literal, and can combine with
* ^J (see case above). So do nothing, and fall
* through into the 'treat it literally' code, */
} else {
/* If we're not in LE_CRLF_NEWLINE mode, then ^M by
* itself acts as a full newline character */
lineedit_complete_line(le);
return;
}
}
}
/*
* If we get to here, we've exhausted the options for treating our
* character as an editing or special function of any kind. Treat
* it as a printing character, or part of one.
*/
lineedit_input_printing_char(le, ch);
}
static void lineedit_input_printing_char(TermLineEditor *le, char ch)
{
/*
* Append ch to the line buffer, either as a new BufChar or by
* adding it to a previous one containing an as yet incomplete
* UTF-8 encoding.
*/
if (le->tail && !le->tail->complete) {
BufChar *bc = le->tail;
/*
* If we're in UTF-8 mode, and ch is a UTF-8 continuation
* byte, then we can append it to bc, which we've just checked
* is missing at least one of those.
*/
if (in_utf(le->term) && (unsigned char)ch - 0x80U < 0x40) {
assert(bc->nwire < lenof(bc->wire));
bc->wire[bc->nwire++] = ch;
lineedit_check_utf8_complete(le, bc);
return;
}
/*
* Otherwise, the previous incomplete character can't be
* extended. Mark it as complete, and if possible, display it
* as a replacement character indicating that something weird
* happened.
*/
bc->complete = true;
if (in_utf(le->term))
lineedit_display_bufchar(le, bc, 0xFFFD);
/*
* But we still haven't processed the byte we're holding. Fall
* through to the next step, where we make a fresh BufChar for
* it.
*/
}
/*
* Make a fresh BufChar.
*/
BufChar *bc = snew(BufChar);
bc->prev = le->tail;
le->tail = bc;
if (bc->prev)
bc->prev->next = bc;
else
le->head = bc;
bc->next = NULL;
bc->complete = false;
bc->space = false;
bc->nwire = 1;
bc->wire[0] = ch;
if (in_utf(le->term)) {
lineedit_check_utf8_complete(le, bc);
} else {
bc->complete = true; /* always, in a single-byte charset */
bc->space = (bc->wire[0] == ' ');
lineedit_display_bufchar(le, bc, CSET_ASCII | (unsigned char)ch);
}
}