Skip to content

Commit 45b2678

Browse files
committed
Allow backspace to wrap cursor to previous line
Fixes #8841
1 parent 55a2f2c commit 45b2678

File tree

14 files changed

+87
-29
lines changed

14 files changed

+87
-29
lines changed

.gitattributes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
kitty/terminfo.h linguist-generated=true
2+
terminfo/kitty.termcap linguist-generated=true
3+
terminfo/kitty.terminfo linguist-generated=true
4+
terminfo/x/xterm-kitty linguist-generated=true
15
kitty/char-props-data.h linguist-generated=true
26
kitty_tests/GraphemeBreakTest.json linguist-generated=true
37
kitty/charsets.c linguist-generated=true

docs/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ Detailed list of changes
118118
- macOS: Fix hiding quick access terminal window not restoring focus to
119119
previously active application (:disc:`8840`)
120120

121+
- 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`)
122+
121123
0.42.2 [2025-07-16]
122124
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
123125

kitty/boss.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ def fmt(x: Any) -> Any:
282282
if isinstance(x, dict):
283283
return json.dumps(x)
284284
return x
285-
safe_print(what, *map(fmt, a))
285+
safe_print(what, *map(fmt, a), flush=True)
286286
# }}}
287287

288288

kitty/screen.c

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,10 +1066,12 @@ draw_control_char(Screen *self, text_loop_state *s, uint32_t ch) {
10661066
switch (ch) {
10671067
case BEL:
10681068
screen_bell(self); break;
1069-
case BS:
1069+
case BS: {
1070+
index_type before = self->cursor->y;
10701071
screen_backspace(self);
1071-
init_segmentation_state(self, s);
1072-
break;
1072+
if (before == self->cursor->y) init_segmentation_state(self, s);
1073+
else init_text_loop_line(self, s);
1074+
} break;
10731075
case HT:
10741076
if (UNLIKELY(self->cursor->x >= self->columns)) {
10751077
if (self->modes.mDECAWM) {
@@ -1889,7 +1891,7 @@ screen_is_cursor_visible(const Screen *self) {
18891891

18901892
void
18911893
screen_backspace(Screen *self) {
1892-
screen_cursor_back(self, 1, -1);
1894+
screen_cursor_move(self, 1, -1);
18931895
}
18941896

18951897
void
@@ -1960,16 +1962,37 @@ screen_set_tab_stop(Screen *self) {
19601962
}
19611963

19621964
void
1963-
screen_cursor_back(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/) {
1965+
screen_cursor_move(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/) {
19641966
if (count == 0) count = 1;
1965-
if (move_direction < 0 && count > self->cursor->x) self->cursor->x = 0;
1966-
else self->cursor->x += move_direction * count;
1967-
screen_ensure_bounds(self, false, cursor_within_margins(self));
1967+
bool in_margins = cursor_within_margins(self);
1968+
if (move_direction > 0) {
1969+
self->cursor->x += count;
1970+
screen_ensure_bounds(self, false, in_margins);
1971+
} else {
1972+
index_type top = in_margins && self->modes.mDECOM ? self->margin_top : 0;
1973+
while (count > 0) {
1974+
if (count <= self->cursor->x) {
1975+
self->cursor->x -= count;
1976+
count = 0;
1977+
} else {
1978+
if (self->cursor->x > 0) {
1979+
count -= self->cursor->x;
1980+
self->cursor->x = 0;
1981+
} else {
1982+
if (self->cursor->y == top) count = 0;
1983+
else {
1984+
count--; self->cursor->y--;
1985+
self->cursor->x = self->columns-1;
1986+
}
1987+
}
1988+
}
1989+
}
1990+
}
19681991
}
19691992

19701993
void
19711994
screen_cursor_forward(Screen *self, unsigned int count/*=1*/) {
1972-
screen_cursor_back(self, count, 1);
1995+
screen_cursor_move(self, count, 1);
19731996
}
19741997

19751998
void
@@ -4437,7 +4460,7 @@ is_using_alternate_linebuf(Screen *self, PyObject *a UNUSED) {
44374460
Py_RETURN_FALSE;
44384461
}
44394462

4440-
WRAP1E(cursor_back, 1, -1)
4463+
WRAP1E(cursor_move, 1, -1)
44414464
WRAP1B(erase_in_line, 0)
44424465
WRAP1B(erase_in_display, 0)
44434466
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[] = {
55385561
MND(reset_dirty, METH_NOARGS)
55395562
MND(is_using_alternate_linebuf, METH_NOARGS)
55405563
MND(is_main_linebuf, METH_NOARGS)
5541-
MND(cursor_back, METH_VARARGS)
5564+
MND(cursor_move, METH_VARARGS)
55425565
MND(erase_in_line, METH_VARARGS)
55435566
MND(erase_in_display, METH_VARARGS)
55445567
MND(clear_scrollback, METH_NOARGS)

kitty/screen.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ void screen_save_modes(Screen *);
186186
void screen_save_mode(Screen *, unsigned int);
187187
bool write_escape_code_to_child(Screen *self, unsigned char which, const char *data);
188188
void screen_cursor_position(Screen*, unsigned int, unsigned int);
189-
void screen_cursor_back(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/);
189+
void screen_cursor_move(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/);
190190
void screen_erase_in_line(Screen *, unsigned int, bool);
191191
void screen_erase_in_display(Screen *, unsigned int, bool);
192192
void screen_draw_text(Screen *self, const uint32_t *chars, size_t num_chars);

kitty/terminfo.h

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

kitty/terminfo.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ def encode_keystring(keybytes: bytes) -> str:
3434
bool_capabilities = {
3535
# auto_right_margin (terminal has automatic margins)
3636
'am',
37+
# auto_left_margin (cursor wraps on CUB1 from 0 to last column on prev line). This prevents ncurses
38+
# from using BS (backspace) to position the cursor. See https://github.com/kovidgoyal/kitty/issues/8841
39+
# It also allows using backspace with multi-line edits in cooked mode. Foot
40+
# is the only other modern terminal I know of that implements this.
41+
'bw',
3742
# can_change (terminal can redefine existing colors)
3843
'ccc',
3944
# has_meta key (i.e. sets the eight bit)
@@ -69,6 +74,7 @@ def encode_keystring(keybytes: bytes) -> str:
6974

7075
termcap_aliases.update({
7176
'am': 'am',
77+
'bw': 'bw',
7278
'cc': 'ccc',
7379
'km': 'km',
7480
'5i': 'mc5i',

kitty/vt-parser.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,7 @@ parse_sgr(Screen *screen, const uint8_t *buf, unsigned int num, const char *repo
10091009
static void
10101010
screen_cursor_up2(Screen *s, unsigned int count) { screen_cursor_up(s, count, false, -1); }
10111011
static void
1012-
screen_cursor_back1(Screen *s, unsigned int count) { screen_cursor_back(s, count, -1); }
1012+
screen_cursor_back1(Screen *s, unsigned int count) { screen_cursor_move(s, count, -1); }
10131013
static void
10141014
screen_tabn(Screen *s, unsigned int count) { for (index_type i=0; i < MAX(1u, count); i++) screen_tab(s); }
10151015

kitty_tests/graphics.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ def test_unicode_placeholders(self):
766766
s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030D")
767767
# These two characters will be two separate refs (not contiguous).
768768
s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030E")
769-
s.cursor_back(4)
769+
s.cursor_move(4)
770770
s.update_only_line_graphics_data()
771771
refs = layers(s)
772772
self.ae(len(refs), 3)
@@ -786,7 +786,7 @@ def test_unicode_placeholders(self):
786786
# The second image, 2x1
787787
s.apply_sgr("38;2;42;43;44")
788788
s.draw("\U0010EEEE\u0305\u030D\U0010EEEE\u0305\u030E")
789-
s.cursor_back(2)
789+
s.cursor_move(2)
790790
s.update_only_line_graphics_data()
791791
refs = layers(s)
792792
self.ae(len(refs), 2)
@@ -804,7 +804,7 @@ def test_unicode_placeholders(self):
804804
s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\U0010EEEE\U0010EEEE\u0305")
805805
# full row 1 of the first image
806806
s.draw("\U0010EEEE\u030D\U0010EEEE\U0010EEEE\U0010EEEE\u030D\u0310")
807-
s.cursor_back(8)
807+
s.cursor_move(8)
808808
s.update_only_line_graphics_data()
809809
refs = layers(s)
810810
self.ae(len(refs), 2)
@@ -826,7 +826,7 @@ def test_unicode_placeholders_3rd_combining_char(self):
826826
# This one will have id=43, which does not exist.
827827
s.apply_sgr("38;2;0;0;43")
828828
s.draw("\U0010EEEE\u0305\U0010EEEE\U0010EEEE\U0010EEEE")
829-
s.cursor_back(4)
829+
s.cursor_move(4)
830830
s.update_only_line_graphics_data()
831831
refs = layers(s)
832832
self.ae(len(refs), 0)
@@ -842,7 +842,7 @@ def test_unicode_placeholders_3rd_combining_char(self):
842842
s.draw("\U0010EEEE\u0305\u0305\u059C\U0010EEEE\u0305\u030D\u059C")
843843
# Check that we can continue by using implicit row/column specification.
844844
s.draw("\U0010EEEE\u0305\U0010EEEE")
845-
s.cursor_back(6)
845+
s.cursor_move(6)
846846
s.update_only_line_graphics_data()
847847
refs = layers(s)
848848
self.ae(len(refs), 2)
@@ -856,7 +856,7 @@ def test_unicode_placeholders_3rd_combining_char(self):
856856
s.draw("\U0010EEEE\u0305\u0305\u0305\U0010EEEE")
857857
s.apply_sgr("38;5;43")
858858
s.draw("\U0010EEEE\u0305\u0305\u059C\U0010EEEE\U0010EEEE\u0305\U0010EEEE")
859-
s.cursor_back(6)
859+
s.cursor_move(6)
860860
s.update_only_line_graphics_data()
861861
refs = layers(s)
862862
self.ae(len(refs), 2)

kitty_tests/parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ def test_csi_codes(self):
353353
s = self.create_screen()
354354
pb = partial(self.parse_bytes_dump, s)
355355
pb('abcde', 'abcde')
356-
s.cursor_back(5)
356+
s.cursor_move(5)
357357
pb('x\033[2@y', 'x', ('screen_insert_characters', 2), 'y')
358358
self.ae(str(s.line(0)), 'xy bc')
359359
pb('x\033[2;7@y', 'x', ('CSI code @ has 2 > 1 parameters',), 'y')

0 commit comments

Comments
 (0)