Allow backspace to wrap cursor to previous line

Fixes #8841
This commit is contained in:
Kovid Goyal
2025-07-23 08:56:54 +05:30
parent 55a2f2c55c
commit 45b2678db1
14 changed files with 87 additions and 29 deletions

4
.gitattributes vendored
View File

@@ -1,3 +1,7 @@
kitty/terminfo.h linguist-generated=true
terminfo/kitty.termcap linguist-generated=true
terminfo/kitty.terminfo linguist-generated=true
terminfo/x/xterm-kitty linguist-generated=true
kitty/char-props-data.h linguist-generated=true
kitty_tests/GraphemeBreakTest.json linguist-generated=true
kitty/charsets.c linguist-generated=true

View File

@@ -118,6 +118,8 @@ Detailed list of changes
- macOS: Fix hiding quick access terminal window not restoring focus to
previously active application (:disc:`8840`)
- Allow using backspace to move the cursor onto the previous line in cooked mode. This is indicated by the `bw` propert in kitty's terminfo (:iss:`8841`)
0.42.2 [2025-07-16]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -282,7 +282,7 @@ class DumpCommands: # {{{
if isinstance(x, dict):
return json.dumps(x)
return x
safe_print(what, *map(fmt, a))
safe_print(what, *map(fmt, a), flush=True)
# }}}

View File

@@ -1066,10 +1066,12 @@ draw_control_char(Screen *self, text_loop_state *s, uint32_t ch) {
switch (ch) {
case BEL:
screen_bell(self); break;
case BS:
case BS: {
index_type before = self->cursor->y;
screen_backspace(self);
init_segmentation_state(self, s);
break;
if (before == self->cursor->y) init_segmentation_state(self, s);
else init_text_loop_line(self, s);
} break;
case HT:
if (UNLIKELY(self->cursor->x >= self->columns)) {
if (self->modes.mDECAWM) {
@@ -1889,7 +1891,7 @@ screen_is_cursor_visible(const Screen *self) {
void
screen_backspace(Screen *self) {
screen_cursor_back(self, 1, -1);
screen_cursor_move(self, 1, -1);
}
void
@@ -1960,16 +1962,37 @@ screen_set_tab_stop(Screen *self) {
}
void
screen_cursor_back(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/) {
screen_cursor_move(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/) {
if (count == 0) count = 1;
if (move_direction < 0 && count > self->cursor->x) self->cursor->x = 0;
else self->cursor->x += move_direction * count;
screen_ensure_bounds(self, false, cursor_within_margins(self));
bool in_margins = cursor_within_margins(self);
if (move_direction > 0) {
self->cursor->x += count;
screen_ensure_bounds(self, false, in_margins);
} else {
index_type top = in_margins && self->modes.mDECOM ? self->margin_top : 0;
while (count > 0) {
if (count <= self->cursor->x) {
self->cursor->x -= count;
count = 0;
} else {
if (self->cursor->x > 0) {
count -= self->cursor->x;
self->cursor->x = 0;
} else {
if (self->cursor->y == top) count = 0;
else {
count--; self->cursor->y--;
self->cursor->x = self->columns-1;
}
}
}
}
}
}
void
screen_cursor_forward(Screen *self, unsigned int count/*=1*/) {
screen_cursor_back(self, count, 1);
screen_cursor_move(self, count, 1);
}
void
@@ -4437,7 +4460,7 @@ is_using_alternate_linebuf(Screen *self, PyObject *a UNUSED) {
Py_RETURN_FALSE;
}
WRAP1E(cursor_back, 1, -1)
WRAP1E(cursor_move, 1, -1)
WRAP1B(erase_in_line, 0)
WRAP1B(erase_in_display, 0)
static PyObject* scroll_until_cursor_prompt(Screen *self, PyObject *args) { int b=false; if(!PyArg_ParseTuple(args, "|p", &b)) return NULL; screen_scroll_until_cursor_prompt(self, b); Py_RETURN_NONE; }
@@ -5538,7 +5561,7 @@ static PyMethodDef methods[] = {
MND(reset_dirty, METH_NOARGS)
MND(is_using_alternate_linebuf, METH_NOARGS)
MND(is_main_linebuf, METH_NOARGS)
MND(cursor_back, METH_VARARGS)
MND(cursor_move, METH_VARARGS)
MND(erase_in_line, METH_VARARGS)
MND(erase_in_display, METH_VARARGS)
MND(clear_scrollback, METH_NOARGS)

View File

@@ -186,7 +186,7 @@ void screen_save_modes(Screen *);
void screen_save_mode(Screen *, unsigned int);
bool write_escape_code_to_child(Screen *self, unsigned char which, const char *data);
void screen_cursor_position(Screen*, unsigned int, unsigned int);
void screen_cursor_back(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/);
void screen_cursor_move(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/);
void screen_erase_in_line(Screen *, unsigned int, bool);
void screen_erase_in_display(Screen *, unsigned int, bool);
void screen_draw_text(Screen *self, const uint32_t *chars, size_t num_chars);

2
kitty/terminfo.h generated

File diff suppressed because one or more lines are too long

View File

@@ -34,6 +34,11 @@ termcap_aliases = {
bool_capabilities = {
# auto_right_margin (terminal has automatic margins)
'am',
# auto_left_margin (cursor wraps on CUB1 from 0 to last column on prev line). This prevents ncurses
# from using BS (backspace) to position the cursor. See https://github.com/kovidgoyal/kitty/issues/8841
# It also allows using backspace with multi-line edits in cooked mode. Foot
# is the only other modern terminal I know of that implements this.
'bw',
# can_change (terminal can redefine existing colors)
'ccc',
# has_meta key (i.e. sets the eight bit)
@@ -69,6 +74,7 @@ bool_capabilities = {
termcap_aliases.update({
'am': 'am',
'bw': 'bw',
'cc': 'ccc',
'km': 'km',
'5i': 'mc5i',

View File

@@ -1009,7 +1009,7 @@ parse_sgr(Screen *screen, const uint8_t *buf, unsigned int num, const char *repo
static void
screen_cursor_up2(Screen *s, unsigned int count) { screen_cursor_up(s, count, false, -1); }
static void
screen_cursor_back1(Screen *s, unsigned int count) { screen_cursor_back(s, count, -1); }
screen_cursor_back1(Screen *s, unsigned int count) { screen_cursor_move(s, count, -1); }
static void
screen_tabn(Screen *s, unsigned int count) { for (index_type i=0; i < MAX(1u, count); i++) screen_tab(s); }

View File

@@ -766,7 +766,7 @@ class TestGraphics(BaseTest):
s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030D")
# These two characters will be two separate refs (not contiguous).
s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030E")
s.cursor_back(4)
s.cursor_move(4)
s.update_only_line_graphics_data()
refs = layers(s)
self.ae(len(refs), 3)
@@ -786,7 +786,7 @@ class TestGraphics(BaseTest):
# The second image, 2x1
s.apply_sgr("38;2;42;43;44")
s.draw("\U0010EEEE\u0305\u030D\U0010EEEE\u0305\u030E")
s.cursor_back(2)
s.cursor_move(2)
s.update_only_line_graphics_data()
refs = layers(s)
self.ae(len(refs), 2)
@@ -804,7 +804,7 @@ class TestGraphics(BaseTest):
s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\U0010EEEE\U0010EEEE\u0305")
# full row 1 of the first image
s.draw("\U0010EEEE\u030D\U0010EEEE\U0010EEEE\U0010EEEE\u030D\u0310")
s.cursor_back(8)
s.cursor_move(8)
s.update_only_line_graphics_data()
refs = layers(s)
self.ae(len(refs), 2)
@@ -826,7 +826,7 @@ class TestGraphics(BaseTest):
# This one will have id=43, which does not exist.
s.apply_sgr("38;2;0;0;43")
s.draw("\U0010EEEE\u0305\U0010EEEE\U0010EEEE\U0010EEEE")
s.cursor_back(4)
s.cursor_move(4)
s.update_only_line_graphics_data()
refs = layers(s)
self.ae(len(refs), 0)
@@ -842,7 +842,7 @@ class TestGraphics(BaseTest):
s.draw("\U0010EEEE\u0305\u0305\u059C\U0010EEEE\u0305\u030D\u059C")
# Check that we can continue by using implicit row/column specification.
s.draw("\U0010EEEE\u0305\U0010EEEE")
s.cursor_back(6)
s.cursor_move(6)
s.update_only_line_graphics_data()
refs = layers(s)
self.ae(len(refs), 2)
@@ -856,7 +856,7 @@ class TestGraphics(BaseTest):
s.draw("\U0010EEEE\u0305\u0305\u0305\U0010EEEE")
s.apply_sgr("38;5;43")
s.draw("\U0010EEEE\u0305\u0305\u059C\U0010EEEE\U0010EEEE\u0305\U0010EEEE")
s.cursor_back(6)
s.cursor_move(6)
s.update_only_line_graphics_data()
refs = layers(s)
self.ae(len(refs), 2)

View File

@@ -353,7 +353,7 @@ class TestParser(BaseTest):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('abcde', 'abcde')
s.cursor_back(5)
s.cursor_move(5)
pb('x\033[2@y', 'x', ('screen_insert_characters', 2), 'y')
self.ae(str(s.line(0)), 'xy bc')
pb('x\033[2;7@y', 'x', ('CSI code @ has 2 > 1 parameters',), 'y')

View File

@@ -44,7 +44,7 @@ class TestScreen(BaseTest):
s.reset(), s.reset_dirty()
s.set_mode(IRM)
s.draw('12345' * 5)
s.cursor_back(5)
s.cursor_move(5)
self.ae(s.cursor.x, 0), self.ae(s.cursor.y, 4)
s.reset_dirty()
s.draw('ab')
@@ -98,7 +98,7 @@ class TestScreen(BaseTest):
s.set_mode(IRM)
s.draw(text * 5)
self.ae(str(s.line(0)), text)
s.cursor_back(5)
s.cursor_move(5)
self.ae(s.cursor.x, 0), self.ae(s.cursor.y, 4)
s.reset_dirty()
s.draw('a\u0306b')
@@ -162,7 +162,7 @@ class TestScreen(BaseTest):
s.reset(), s.reset_dirty()
s.draw('abcde')
s.cursor.bold = True
s.cursor_back(4)
s.cursor_move(4)
s.reset_dirty()
self.ae(s.cursor.x, 1)
@@ -170,11 +170,11 @@ class TestScreen(BaseTest):
s.insert_characters(2)
self.ae(str(s.line(0)), 'a bc')
self.assertTrue(s.line(0).cursor_from(1).bold)
s.cursor_back(1)
s.cursor_move(1)
s.insert_characters(20)
self.ae(str(s.line(0)), '')
s.draw('xココ')
s.cursor_back(5)
s.cursor_move(5)
s.reset_dirty()
s.insert_characters(1)
self.ae(str(s.line(0)), ' xコ')
@@ -270,7 +270,7 @@ class TestScreen(BaseTest):
self.ae((s.cursor.x, s.cursor.y), (0, 1))
s.cursor_forward(3)
self.ae((s.cursor.x, s.cursor.y), (3, 1))
s.cursor_back()
s.cursor_move()
self.ae((s.cursor.x, s.cursor.y), (2, 1))
s.cursor_down()
self.ae((s.cursor.x, s.cursor.y), (2, 2))
@@ -534,6 +534,28 @@ class TestScreen(BaseTest):
s.draw('aaaX\tbbbb')
self.ae(str(s.line(0)) + str(s.line(1)), 'aaaXbbbb')
def test_backspace(self):
s = self.create_screen()
q = 'a'*s.columns
def backspace(use_bs=True):
if use_bs: # this is how the kernel implements backspace
s.draw('\x08 \x08')
else:
s.cursor_move(1)
s.draw(' ')
s.cursor_move(1)
for use_bs in (True, False):
s.reset()
s.draw(q)
s.draw('b')
backspace(use_bs)
self.ae(str(s.line(0)), q)
self.ae(str(s.line(1)), ' ')
self.ae(s.cursor.x, 0)
backspace(use_bs)
self.ae(str(s.line(0)), q[:-1] + ' ')
self.ae(str(s.line(1)), ' ')
def test_margins(self):
# Taken from vttest/main.c
s = self.create_screen(cols=80, lines=24)

View File

@@ -1 +1 @@
xterm-kitty|KovIdTTY:5i:NP:am:cc:hs:km:mi:ms:xn:Co#256:co#80:it#8:li#24:pa#32767:#2=\E[1;2H:#3=\E[2;2~:#4=\E[1;2D:%1=:%c=\E[6;2~:%e=\E[5;2~:%i=\E[1;2C:&8=:&9=\E[1;2E:*4=\E[3;2~:*7=\E[1;2F:@1=\EOE:@7=\EOF:AB=\E[48;5;%dm:AF=\E[38;5;%dm:AL=\E[%dL:DC=\E[%dP:DL=\E[%dM:DO=\E[%dB:F1=\E[23~:F2=\E[24~:F3=\E[1;2P:F4=\E[1;2Q:F5=\E[13;2~:F6=\E[1;2S:F7=\E[15;2~:F8=\E[17;2~:F9=\E[18;2~:FA=\E[19;2~:FB=\E[20;2~:FC=\E[21;2~:FD=\E[23;2~:FE=\E[24;2~:FF=\E[1;5P:FG=\E[1;5Q:FH=\E[13;5~:FI=\E[1;5S:FJ=\E[15;5~:FK=\E[17;5~:FL=\E[18;5~:FM=\E[19;5~:FN=\E[20;5~:FO=\E[21;5~:FP=\E[23;5~:FQ=\E[24;5~:FR=\E[1;6P:FS=\E[1;6Q:FT=\E[13;6~:FU=\E[1;6S:FV=\E[15;6~:FW=\E[17;6~:FX=\E[18;6~:FY=\E[19;6~:FZ=\E[20;6~:Fa=\E[21;6~:Fb=\E[23;6~:Fc=\E[24;6~:Fd=\E[1;3P:Fe=\E[1;3Q:Ff=\E[13;3~:Fg=\E[1;3S:Fh=\E[15;3~:Fi=\E[17;3~:Fj=\E[18;3~:Fk=\E[19;3~:Fl=\E[20;3~:Fm=\E[21;3~:Fn=\E[23;3~:Fo=\E[24;3~:Fp=\E[1;4P:Fq=\E[1;4Q:Fr=\E[13;4~:IC=\E[%d@:..Ic=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\:K1=:K3=:K4=:K5=:Km=\E[M:LE=\E[%dD:RA=\E[?7l:RI=\E[%dC:SA=\E[?7h:SF=\E[%dS:SR=\E[%dT:UP=\E[%dA:ZH=\E[3m:ZR=\E[23m:ac=++,,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~:ae=\E(B:al=\E[L:as=\E(0:bl=^G:bt=\E[Z:cb=\E[1K:cd=\E[J:ce=\E[K:ch=\E[%i%dG:cl=\E[H\E[2J:cm=\E[%i%d;%dH:cr=\r:cs=\E[%i%d;%dr:ct=\E[3g:cv=\E[%i%dd:dc=\E[P:dl=\E[M:do=\n:ds=\E]2;\E\\:ec=\E[%dX:ei=\E[4l:fs=^G:ho=\E[H:im=\E[4h:k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\E[15~:k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k;=\E[21~:kB=\E[Z:kD=\E[3~:kF=\E[1;2B:kI=\E[2~:kN=\E[6~:kP=\E[5~:kR=\E[1;2A:kb=\177:kd=\EOB:ke=\E[?1l:kh=\EOH:kl=\EOD:kr=\EOC:ks=\E[?1h:ku=\EOA:le=^H:md=\E[1m:me=\E[0m:mh=\E[2m:mr=\E[7m:nd=\E[C:oc=\E]104\007:op=\E[39;49m:r1=\E]\E\\\Ec:rc=\E8:..rp=%p1%c\E[%p2%{1}%-%db:..sa=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m:sc=\E7:se=\E[27m:sf=\n:so=\E[7m:sr=\EM:st=\EH:ta=^I:te=\E[?1049l:ti=\E[?1049h:ts=\E]2;:u6=\E[%i%d;%dR:u7=\E[6n:..u8=\E[?%[;0123456789]c:u9=\E[c:ue=\E[24m:up=\E[A:us=\E[4m:vb=\E[?5h\E[?5l:ve=\E[?12h\E[?25h:vi=\E[?25l:vs=\E[?12;25h:
xterm-kitty|KovIdTTY:5i:NP:am:bw:cc:hs:km:mi:ms:xn:Co#256:co#80:it#8:li#24:pa#32767:#2=\E[1;2H:#3=\E[2;2~:#4=\E[1;2D:%1=:%c=\E[6;2~:%e=\E[5;2~:%i=\E[1;2C:&8=:&9=\E[1;2E:*4=\E[3;2~:*7=\E[1;2F:@1=\EOE:@7=\EOF:AB=\E[48;5;%dm:AF=\E[38;5;%dm:AL=\E[%dL:DC=\E[%dP:DL=\E[%dM:DO=\E[%dB:F1=\E[23~:F2=\E[24~:F3=\E[1;2P:F4=\E[1;2Q:F5=\E[13;2~:F6=\E[1;2S:F7=\E[15;2~:F8=\E[17;2~:F9=\E[18;2~:FA=\E[19;2~:FB=\E[20;2~:FC=\E[21;2~:FD=\E[23;2~:FE=\E[24;2~:FF=\E[1;5P:FG=\E[1;5Q:FH=\E[13;5~:FI=\E[1;5S:FJ=\E[15;5~:FK=\E[17;5~:FL=\E[18;5~:FM=\E[19;5~:FN=\E[20;5~:FO=\E[21;5~:FP=\E[23;5~:FQ=\E[24;5~:FR=\E[1;6P:FS=\E[1;6Q:FT=\E[13;6~:FU=\E[1;6S:FV=\E[15;6~:FW=\E[17;6~:FX=\E[18;6~:FY=\E[19;6~:FZ=\E[20;6~:Fa=\E[21;6~:Fb=\E[23;6~:Fc=\E[24;6~:Fd=\E[1;3P:Fe=\E[1;3Q:Ff=\E[13;3~:Fg=\E[1;3S:Fh=\E[15;3~:Fi=\E[17;3~:Fj=\E[18;3~:Fk=\E[19;3~:Fl=\E[20;3~:Fm=\E[21;3~:Fn=\E[23;3~:Fo=\E[24;3~:Fp=\E[1;4P:Fq=\E[1;4Q:Fr=\E[13;4~:IC=\E[%d@:..Ic=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\:K1=:K3=:K4=:K5=:Km=\E[M:LE=\E[%dD:RA=\E[?7l:RI=\E[%dC:SA=\E[?7h:SF=\E[%dS:SR=\E[%dT:UP=\E[%dA:ZH=\E[3m:ZR=\E[23m:ac=++,,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~:ae=\E(B:al=\E[L:as=\E(0:bl=^G:bt=\E[Z:cb=\E[1K:cd=\E[J:ce=\E[K:ch=\E[%i%dG:cl=\E[H\E[2J:cm=\E[%i%d;%dH:cr=\r:cs=\E[%i%d;%dr:ct=\E[3g:cv=\E[%i%dd:dc=\E[P:dl=\E[M:do=\n:ds=\E]2;\E\\:ec=\E[%dX:ei=\E[4l:fs=^G:ho=\E[H:im=\E[4h:k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\E[15~:k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k;=\E[21~:kB=\E[Z:kD=\E[3~:kF=\E[1;2B:kI=\E[2~:kN=\E[6~:kP=\E[5~:kR=\E[1;2A:kb=\177:kd=\EOB:ke=\E[?1l:kh=\EOH:kl=\EOD:kr=\EOC:ks=\E[?1h:ku=\EOA:le=^H:md=\E[1m:me=\E[0m:mh=\E[2m:mr=\E[7m:nd=\E[C:oc=\E]104\007:op=\E[39;49m:r1=\E]\E\\\Ec:rc=\E8:..rp=%p1%c\E[%p2%{1}%-%db:..sa=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m:sc=\E7:se=\E[27m:sf=\n:so=\E[7m:sr=\EM:st=\EH:ta=^I:te=\E[?1049l:ti=\E[?1049h:ts=\E]2;:u6=\E[%i%d;%dR:u7=\E[6n:..u8=\E[?%[;0123456789]c:u9=\E[c:ue=\E[24m:up=\E[A:us=\E[4m:vb=\E[?5h\E[?5l:ve=\E[?12h\E[?25h:vi=\E[?25l:vs=\E[?12;25h:

View File

@@ -3,6 +3,7 @@ xterm-kitty|KovIdTTY,
Tc,
XF,
am,
bw,
ccc,
fullkbd,
hs,

BIN
terminfo/x/xterm-kitty generated

Binary file not shown.