Control: tags 1138246 + patch
Control: tags 1138246 + pending

Dear maintainer,

I've prepared an NMU for libtsm (versioned as 4.5.0-0.1) and
uploaded it to DELAYED/5. Please feel free to tell me if I
should cancel it.

Regards.

diffstat for libtsm-4.4.3 libtsm-4.5.0

 NEWS.md                    |   20 +-
 debian/changelog           |    9
 debian/control             |    2
 debian/libtsm4.symbols     |    3
 external/wcwidth/wcwidth.h |    9
 meson.build                |    2
 src/shared/shl_dlist.h     |  132 ++++++++++++++
 src/tsm/libtsm-int.h       |   36 ++-
 src/tsm/libtsm.h           |   15 +
 src/tsm/libtsm.sym         |    6
 src/tsm/tsm-render.c       |   33 +--
 src/tsm/tsm-screen.c       |  321 ++++++++++++++--------------------
 src/tsm/tsm-selection.c    |  420 +++++++++++++++------------------------------
 src/tsm/tsm-vte.c          |   50 ++++-
 test/test_screen.c         |   28 +++
 test/test_selection.c      |  146 ++++++++++-----
 test/test_vte.c            |  210 ++++++++++++++++++++++
 17 files changed, 891 insertions(+), 551 deletions(-)

diff -Nru libtsm-4.4.3/debian/changelog libtsm-4.5.0/debian/changelog
--- libtsm-4.4.3/debian/changelog    2026-04-03 19:06:22.000000000 -0400
+++ libtsm-4.5.0/debian/changelog    2026-06-03 13:54:39.000000000 -0400
@@ -1,3 +1,12 @@
+libtsm (4.5.0-0.1) unstable; urgency=medium
+
+  * Non-maintainer upload.
+  * New upstream release. (Closes: #1138246)
+  * debian/control: Bump Standards-Version to 4.7.4.
+  * debian/libtsm4.symbols: Record new symbols.
+
+ -- Boyuan Yang <[email protected]>  Wed, 03 Jun 2026 13:54:39 -0400
+
 libtsm (4.4.3-0.1) unstable; urgency=medium

   * Non-maintainer upload.
diff -Nru libtsm-4.4.3/debian/control libtsm-4.5.0/debian/control
--- libtsm-4.4.3/debian/control    2026-04-03 19:03:08.000000000 -0400
+++ libtsm-4.5.0/debian/control    2026-06-03 13:48:50.000000000 -0400
@@ -7,7 +7,7 @@
                debhelper-compat (= 13),
                libxkbcommon-dev,
                pkgconf,
-Standards-Version: 4.7.2
+Standards-Version: 4.7.4
 Homepage: https://github.com/kmscon/libtsm
 Vcs-Browser: https://salsa.debian.org/debian/libtsm
 Vcs-Git: https://salsa.debian.org/debian/libtsm.git
diff -Nru libtsm-4.4.3/debian/libtsm4.symbols 
libtsm-4.5.0/debian/libtsm4.symbols
--- libtsm-4.4.3/debian/libtsm4.symbols    2026-04-03 19:03:08.000000000 -0400
+++ libtsm-4.5.0/debian/libtsm4.symbols    2026-06-03 13:54:36.000000000 -0400
@@ -7,6 +7,7 @@
  LIBTSM_4_1@LIBTSM_4_1 4.3.0
  LIBTSM_4_3@LIBTSM_4_3 4.3.0
  LIBTSM_4_4@LIBTSM_4_4 4.4.2
+ LIBTSM_4_5@LIBTSM_4_5 4.5.0
  tsm_screen_clear_sb@LIBTSM_3 4.3.0
  tsm_screen_delete_chars@LIBTSM_3 4.3.0
  tsm_screen_delete_lines@LIBTSM_3 4.3.0
@@ -80,7 +81,9 @@
  tsm_vte_ref@LIBTSM_3 4.3.0
  tsm_vte_reset@LIBTSM_3 4.3.0
  tsm_vte_set_backspace_sends_delete@LIBTSM_4_1 4.3.0
+ tsm_vte_set_bell_cb@LIBTSM_4_5 4.5.0
  tsm_vte_set_custom_palette@LIBTSM_4 4.3.0
+ tsm_vte_set_led_cb@LIBTSM_4_5 4.5.0
  tsm_vte_set_mouse_cb@LIBTSM_4_1 4.3.0
  tsm_vte_set_osc_cb@LIBTSM_3 4.3.0
  tsm_vte_set_palette@LIBTSM_3 4.3.0
diff -Nru libtsm-4.4.3/external/wcwidth/wcwidth.h 
libtsm-4.5.0/external/wcwidth/wcwidth.h
--- libtsm-4.4.3/external/wcwidth/wcwidth.h    2026-03-20 09:44:52.000000000 
-0400
+++ libtsm-4.5.0/external/wcwidth/wcwidth.h    2026-04-21 05:27:37.000000000 
-0400
@@ -3,10 +3,15 @@

 #include <stdlib.h>

-__BEGIN_DECLS
+#ifdef __cplusplus
+extern "C" {
+#endif

 int wcwidth(wchar_t ucs);

-__END_DECLS
+#ifdef __cplusplus
+}
+#endif
+

 #endif
diff -Nru libtsm-4.4.3/meson.build libtsm-4.5.0/meson.build
--- libtsm-4.4.3/meson.build    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/meson.build    2026-04-21 05:27:37.000000000 -0400
@@ -3,7 +3,7 @@
 project(
     'libtsm',
     'c',
-    version: '4.4.3',
+    version: '4.5.0',
     license: 'MIT',
     meson_version: '>=1.1',
     default_options: [
diff -Nru libtsm-4.4.3/NEWS.md libtsm-4.5.0/NEWS.md
--- libtsm-4.4.3/NEWS.md    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/NEWS.md    2026-04-21 05:27:37.000000000 -0400
@@ -1,5 +1,21 @@
 # libtsm Release News

+## CHANGES WITH 4.5.0
+### New features
+* Support for terminal bell and keyboard LEDs by @aruiz in 
https://github.com/kmscon/libtsm/pull/34
+### Bug fixes
+* Fix build musl by @kdj0c in https://github.com/kmscon/libtsm/pull/32
+* test/vte: fix memory leak in vte tests by @kdj0c in 
https://github.com/kmscon/libtsm/pull/35
+* Refactor scrollback and selection by @kdj0c in 
https://github.com/kmscon/libtsm/pull/36
+* screen: Fix wrong attribute for new cells when resizing by @kdj0c in 
https://github.com/kmscon/libtsm/pull/37
+* Fix remove from sb by @kdj0c in https://github.com/kmscon/libtsm/pull/38
+* Fix get next line by @kdj0c in https://github.com/kmscon/libtsm/pull/39
+* Fix scrollback position by @kdj0c in https://github.com/kmscon/libtsm/pull/40
+* test: robustness, make the test faster. by @kdj0c in 
https://github.com/kmscon/libtsm/pull/41
+
+### New Contributors
+* @aruiz made their first contribution in 
https://github.com/kmscon/libtsm/pull/34
+
 ## CHANGES WITH 4.4.3
 ### New features
 * Add support for VT200 mouse tracking by @caramelli in 
https://github.com/kmscon/libtsm/pull/27
@@ -9,7 +25,7 @@
 * vte: guard case 'm' (SGR) against CSI_GT prefix by @kdj0c in 
https://github.com/kmscon/libtsm/pull/29
 * wcwidth: update to upstream v4 by @kdj0c in 
https://github.com/kmscon/libtsm/pull/28

-## New Contributors
+### New Contributors
 * @Karlson2k made their first contribution in 
https://github.com/kmscon/libtsm/pull/24
 * @caramelli made their first contribution in 
https://github.com/kmscon/libtsm/pull/26

@@ -21,7 +37,7 @@
 * Fix CSI 18t and 19t reporting size. by @kdj0c in 
https://github.com/kmscon/libtsm/pull/20
 * resize: Fix a corner case when resizing by @kdj0c in 
https://github.com/kmscon/libtsm/pull/22

-## New Contributors
+### New Contributors
 * @1ace made their first contribution in 
https://github.com/kmscon/libtsm/pull/19

 ## CHANGES WITH 4.4.1
diff -Nru libtsm-4.4.3/src/shared/shl_dlist.h 
libtsm-4.5.0/src/shared/shl_dlist.h
--- libtsm-4.4.3/src/shared/shl_dlist.h    1969-12-31 19:00:00.000000000 -0500
+++ libtsm-4.5.0/src/shared/shl_dlist.h    2026-04-21 05:27:37.000000000 -0400
@@ -0,0 +1,132 @@
+/*
+ * shl - Double Linked List
+ *
+ * Copyright (c) 2011-2012 David Herrmann <[email protected]>
+ * Copyright (c) 2011 University of Tuebingen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files
+ * (the "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * A simple double linked list implementation
+ */
+
+#ifndef SHL_DLIST_H
+#define SHL_DLIST_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+/* miscellaneous */
+
+#define shl_offsetof(pointer, type, member)                                   \
+    ({                              \
+        const typeof(((type *)0)->member) *__ptr = (pointer);                  
           \
+        (type *)(((char *)__ptr) - offsetof(type, member));                    
       \
+    })
+
+/* double linked list */
+
+struct shl_dlist {
+    struct shl_dlist *next;
+    struct shl_dlist *prev;
+};
+
+#define SHL_DLIST_INIT(head) {&(head), &(head)}
+
+static inline void shl_dlist_init(struct shl_dlist *list)
+{
+    list->next = list;
+    list->prev = list;
+}
+
+static inline void shl_dlist__link(struct shl_dlist *prev, struct shl_dlist 
*next,
+                   struct shl_dlist *n)
+{
+    next->prev = n;
+    n->next = next;
+    n->prev = prev;
+    prev->next = n;
+}
+
+static inline void shl_dlist_link(struct shl_dlist *head, struct shl_dlist *n)
+{
+    return shl_dlist__link(head, head->next, n);
+}
+
+static inline void shl_dlist_link_tail(struct shl_dlist *head, struct 
shl_dlist *n)
+{
+    return shl_dlist__link(head->prev, head, n);
+}
+
+static inline void shl_dlist__unlink(struct shl_dlist *prev, struct shl_dlist 
*next)
+{
+    next->prev = prev;
+    prev->next = next;
+}
+
+static inline void shl_dlist_unlink(struct shl_dlist *e)
+{
+    shl_dlist__unlink(e->prev, e->next);
+    e->prev = NULL;
+    e->next = NULL;
+}
+
+static inline bool shl_dlist_empty(struct shl_dlist *head)
+{
+    return head->next == head;
+}
+
+#define shl_dlist_entry(ptr, type, member) shl_offsetof((ptr), type, member)
+
+#define shl_dlist_first(head, type, member) shl_dlist_entry((head)->next, 
type, member)
+
+#define shl_dlist_last(head, type, member) shl_dlist_entry((head)->prev, type, 
member)
+
+#define shl_dlist_next(iter, head, member) \
+    ((iter)->member.next == (head) ? NULL : 
shl_dlist_entry((iter)->member.next, typeof(*iter), list))
+
+#define shl_dlist_prev(iter, head, member) \
+    ((iter)->member.prev == (head) ? NULL : 
shl_dlist_entry((iter)->member.prev, typeof(*iter), list))
+
+#define shl_dlist_for_each(iter, head) for (iter = (head)->next; iter != (head); 
iter = iter->next)
+
+#define shl_dlist_for_each_but_one(iter, start, head)                          
         \
+    for (iter = ((start)->next == (head)) ? (start)->next->next : 
(start)->next;               \
+         iter != (start);                               \
+         iter = (iter->next == (head) && (start) != (head)) ? iter->next->next : 
iter->next)
+
+#define shl_dlist_for_each_safe(iter, tmp, head)                               
   \
+    for (iter = (head)->next, tmp = iter->next; iter != (head); iter = tmp, tmp = 
iter->next)
+
+#define shl_dlist_for_each_reverse(iter, head)                                 
 \
+    for (iter = (head)->prev; iter != (head); iter = iter->prev)
+
+#define shl_dlist_for_each_reverse_but_one(iter, start, head)                  
                 \
+    for (iter = ((start)->prev == (head)) ? (start)->prev->prev : 
(start)->prev;               \
+         iter != (start);                               \
+         iter = (iter->prev == (head) && (start) != (head)) ? iter->prev->prev : 
iter->prev)
+
+#define shl_dlist_for_each_reverse_safe(iter, tmp, head)                       
           \
+    for (iter = (head)->prev, tmp = iter->prev; iter != (head); iter = tmp, tmp = 
iter->prev)
+
+#endif /* SHL_DLIST_H */
diff -Nru libtsm-4.4.3/src/tsm/libtsm.h libtsm-4.5.0/src/tsm/libtsm.h
--- libtsm-4.4.3/src/tsm/libtsm.h    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/src/tsm/libtsm.h    2026-04-21 05:27:37.000000000 -0400
@@ -418,6 +418,19 @@
                   bool track_pixels,
                   void *data);

+typedef void (*tsm_vte_bell_cb) (struct tsm_vte *vte,
+                 void *data);
+
+enum tsm_vte_led {
+    TSM_VTE_LED_SCROLL_LOCK = (1 << 0),
+    TSM_VTE_LED_NUM_LOCK    = (1 << 1),
+    TSM_VTE_LED_CAPS_LOCK   = (1 << 2),
+};
+
+typedef void (*tsm_vte_led_cb) (struct tsm_vte *vte,
+                unsigned int leds,
+                void *data);
+
 int tsm_vte_new(struct tsm_vte **out, struct tsm_screen *con,
         tsm_vte_write_cb write_cb, void *data,
         tsm_log_t log, void *log_data);
@@ -426,6 +439,8 @@

 void tsm_vte_set_osc_cb(struct tsm_vte *vte, tsm_vte_osc_cb osc_cb, void 
*osc_data);
 void tsm_vte_set_mouse_cb(struct tsm_vte *vte, tsm_vte_mouse_cb mouse_cb, void 
*mouse_data);
+void tsm_vte_set_bell_cb(struct tsm_vte *vte, tsm_vte_bell_cb bell_cb, void 
*bell_data);
+void tsm_vte_set_led_cb(struct tsm_vte *vte, tsm_vte_led_cb led_cb, void 
*led_data);

 /**
  * @brief Set color palette to one of the predefined palette on the vte object.
diff -Nru libtsm-4.4.3/src/tsm/libtsm-int.h libtsm-4.5.0/src/tsm/libtsm-int.h
--- libtsm-4.4.3/src/tsm/libtsm-int.h    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/src/tsm/libtsm-int.h    2026-04-21 05:27:37.000000000 -0400
@@ -32,6 +32,7 @@
 #include <stdlib.h>
 #include <stdint.h>
 #include "libtsm.h"
+#include "shl_dlist.h"
 #include "shl-llog.h"

 #define SHL_EXPORT __attribute__((visibility("default")))
@@ -87,20 +88,26 @@
 };

 struct line {
-    struct line *next;        /* next line (NULL if not sb) */
-    struct line *prev;        /* prev line (NULL if not sb) */
-
+    struct shl_dlist list;        /* list node, next/prev are NULL if not in 
sb */
     unsigned int size;        /* real width */
     struct cell *cells;        /* actuall cells */
-    uint64_t sb_id;            /* sb ID */
+    uint64_t sb_id;            /* sb ID, 0 if not in sb */
     tsm_age_t age;            /* age of the whole line */
 };

-#define SELECTION_TOP -1
 struct selection_pos {
-    struct line *line;
-    unsigned int x;
-    int y;
+    unsigned int x;            /* x offset from the start of the line */
+    struct line *line;        /* line the selection is on */
+};
+
+struct tsm_scrollback {
+    /* scroll-back buffer */
+    unsigned int count;        /* number of lines in sb */
+    struct shl_dlist list;    /* list of lines in sb */
+    unsigned int max;        /* max-limit of lines in sb */
+    struct line *pos;        /* current position in sb or NULL */
+    unsigned int pos_num;    /* current numeric position in sb */
+    uint64_t last_id;        /* last id given to sb-line */
 };

 struct tsm_screen {
@@ -134,14 +141,7 @@
     struct line **alt_lines;    /* real alternative lines */
     tsm_age_t age;            /* whole screen age */

-    /* scroll-back buffer */
-    unsigned int sb_count;        /* number of lines in sb */
-    struct line *sb_first;        /* first line; was moved first */
-    struct line *sb_last;        /* last line; was moved last*/
-    unsigned int sb_max;        /* max-limit of lines in sb */
-    struct line *sb_pos;        /* current position in sb or NULL */
-    unsigned int sb_pos_num;    /* current numeric position in sb */
-    uint64_t sb_last_id;        /* last id given to sb-line */
+    struct tsm_scrollback sb;

     /* cursor: positions are always in-bound, but cursor_x might be
      * bigger than size_x if new-line is pending */
@@ -172,6 +172,10 @@
     }
 }

+static inline bool is_in_scrollback(struct selection_pos *sel) {
+    return (sel->line && sel->line->sb_id);
+}
+
 /* available character sets */

 typedef tsm_symbol_t tsm_vte_charset[96];
diff -Nru libtsm-4.4.3/src/tsm/libtsm.sym libtsm-4.5.0/src/tsm/libtsm.sym
--- libtsm-4.4.3/src/tsm/libtsm.sym    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/src/tsm/libtsm.sym    2026-04-21 05:27:37.000000000 -0400
@@ -142,3 +142,9 @@
 global:
     tsm_vte_paste;
 } LIBTSM_4_3;
+
+LIBTSM_4_5 {
+global:
+    tsm_vte_set_bell_cb;
+    tsm_vte_set_led_cb;
+} LIBTSM_4_4;
diff -Nru libtsm-4.4.3/src/tsm/tsm-render.c libtsm-4.5.0/src/tsm/tsm-render.c
--- libtsm-4.4.3/src/tsm/tsm-render.c    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/src/tsm/tsm-render.c    2026-04-21 05:27:37.000000000 -0400
@@ -38,6 +38,7 @@
 #include "libtsm.h"
 #include "libtsm-int.h"
 #include "shl-llog.h"
+#include "shl_dlist.h"

 #define LLOG_SUBSYSTEM "tsm-render"

@@ -47,7 +48,7 @@
 {
     unsigned int cur_x, cur_y;
     unsigned int i, j, k;
-    struct line *iter, *line = NULL;
+    struct line *line, *next_line = NULL;
     struct cell *cell, empty;
     struct tsm_screen_attr attr;
     int ret, warned = 0;
@@ -71,47 +72,41 @@
         cur_y = con->size_y - 1;

     /* push each character into rendering pipeline */
-
-    iter = con->sb_pos;
     k = 0;
+    next_line = con->sb.pos;

     if (con->sel_active) {
-        if (!con->sel_start.line && con->sel_start.y == SELECTION_TOP)
+        if (!con->sel_start.line)
             in_sel = !in_sel;
-        if (!con->sel_end.line && con->sel_end.y == SELECTION_TOP)
+        if (!con->sel_end.line)
             in_sel = !in_sel;

-        if (con->sel_start.line &&
-            (!iter || con->sel_start.line->sb_id < iter->sb_id))
+        if (is_in_scrollback(&con->sel_start) &&
+            (!con->sb.pos || con->sel_start.line->sb_id < con->sb.pos->sb_id))
             in_sel = !in_sel;
-        if (con->sel_end.line &&
-            (!iter || con->sel_end.line->sb_id < iter->sb_id))
+        if (is_in_scrollback(&con->sel_end) &&
+            (!con->sb.pos || con->sel_end.line->sb_id < con->sb.pos->sb_id))
             in_sel = !in_sel;
     }

     for (i = 0; i < con->size_y; ++i) {
-        if (iter) {
-            line = iter;
-            iter = iter->next;
+        if (next_line) {
+            line = next_line;
+            next_line = shl_dlist_next(next_line, &con->sb.list, list);
         } else {
             line = con->lines[k];
             k++;
         }

         if (con->sel_active) {
-            if (con->sel_start.line == line ||
-                (!con->sel_start.line &&
-                 con->sel_start.y == k - 1))
+            if (con->sel_start.line == line)
                 sel_start = true;
             else
                 sel_start = false;
-            if (con->sel_end.line == line ||
-                (!con->sel_end.line &&
-                 con->sel_end.y == k - 1))
+            if (con->sel_end.line == line)
                 sel_end = true;
             else
                 sel_end = false;
-
             was_sel = false;
         }

diff -Nru libtsm-4.4.3/src/tsm/tsm-screen.c libtsm-4.5.0/src/tsm/tsm-screen.c
--- libtsm-4.4.3/src/tsm/tsm-screen.c    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/src/tsm/tsm-screen.c    2026-04-21 05:27:37.000000000 -0400
@@ -64,6 +64,8 @@
 #include "libtsm.h"
 #include "libtsm-int.h"
 #include "shl-llog.h"
+#include "shl-macro.h"
+#include "shl_dlist.h"

 #define LLOG_SUBSYSTEM "tsm-screen"

@@ -110,7 +112,7 @@
     c->age = con->age_cnt;
 }

-void screen_cell_init_generic(struct tsm_screen *con, struct cell *cell, 
struct tsm_screen_attr *attr)
+static void screen_cell_init_generic(struct tsm_screen *con, struct cell 
*cell, struct tsm_screen_attr *attr)
 {
     cell->ch = 0;
     cell->width = 1;
@@ -136,8 +138,9 @@
     line = malloc(sizeof(*line));
     if (!line)
         return -ENOMEM;
-    line->next = NULL;
-    line->prev = NULL;
+    line->list.next = NULL;
+    line->list.prev = NULL;
+    line->sb_id = 0;
     line->size = width;
     line->age = con->age_cnt;

@@ -184,6 +187,16 @@
     return 0;
 }

+static void clear_selection_on_line(struct tsm_screen *con, struct line *line)
+{
+    if (!con->sel_active)
+        return;
+    if (con->sel_start.line == line)
+        con->sel_start.line = NULL;
+    if (con->sel_end.line == line)
+        con->sel_end.line = NULL;
+}
+
 /* This links the given line into the scrollback-buffer */
 static void link_to_scrollback(struct tsm_screen *con, struct line *line)
 {
@@ -192,118 +205,81 @@
     /* TODO: more sophisticated ageing */
     con->age = con->age_cnt;

-    if (con->sb_max == 0) {
-        if (con->sel_active) {
-            if (con->sel_start.line == line) {
-                con->sel_start.line = NULL;
-                con->sel_start.y = SELECTION_TOP;
-            }
-            if (con->sel_end.line == line) {
-                con->sel_end.line = NULL;
-                con->sel_end.y = SELECTION_TOP;
-            }
-        }
+    if (con->sb.max == 0) {
+        clear_selection_on_line(con, line);
         line_free(line);
         return;
     }

     /* Remove a line from the scrollback buffer if it reaches its maximum.
      * We must take care to correctly keep the current position as the new
-     * line is linked in after we remove the top-most line here.
-     * sb_max == 0 is tested earlier so we can assume sb_max > 0 here. In
-     * other words, buf->sb_first is a valid line if sb_count >= sb_max. */
-    if (con->sb_count >= con->sb_max) {
-        tmp = con->sb_first;
-        con->sb_first = tmp->next;
-        if (tmp->next)
-            tmp->next->prev = NULL;
-        else
-            con->sb_last = NULL;
-        --con->sb_count;
-
-        /* (position == tmp && !next) means we have sb_max=1 so set
-         * position to the new line. Otherwise, set to new first line.
-         * If position!=tmp and we have a fixed-position then nothing
-         * needs to be done because we can stay at the same line. If we
-         * have no fixed-position, we need to set the position to the
-         * next inserted line, which can be "line", too. */
-        if (con->sb_pos) {
-            if (con->sb_pos == tmp ||
-                !(con->flags & TSM_SCREEN_FIXED_POS)) {
-                if (con->sb_pos->next) {
-                    con->sb_pos = con->sb_pos->next;
-                    ++con->sb_pos_num;
-                } else {
-                    con->sb_pos = line;
-                    con->sb_pos_num = 0;
-                }
-            }
-        }
-
-        if (con->sel_active) {
-            if (con->sel_start.line == tmp) {
-                con->sel_start.line = NULL;
-                con->sel_start.y = SELECTION_TOP;
-            }
-            if (con->sel_end.line == tmp) {
-                con->sel_end.line = NULL;
-                con->sel_end.y = SELECTION_TOP;
-            }
+     * line is linked in after we remove the top-most line here. */
+    if (con->sb.count >= con->sb.max) {
+        tmp = shl_dlist_first(&con->sb.list, struct line, list);
+        shl_dlist_unlink(&tmp->list);
+        --con->sb.count;
+
+        /* Only consider sb.max > 1, so there is always another line in sb. */
+        if (con->sb.pos == tmp) {
+            con->sb.pos = shl_dlist_first(&con->sb.list, struct line, list);
+            con->sb.pos_num = 0;
+        } else {
+            con->sb.pos_num--;
         }
+        clear_selection_on_line(con, tmp);
         line_free(tmp);
     }

-    line->sb_id = ++con->sb_last_id;
-    line->next = NULL;
-    line->prev = con->sb_last;
-    if (con->sb_last) {
-        con->sb_last->next = line;
-    } else {
-        con->sb_first = line;
-    }
-    con->sb_last = line;
-    ++con->sb_count;
-
-    if (con->sb_pos == NULL) {
-        con->sb_pos_num = con->sb_count;
-    }
+    line->sb_id = ++con->sb.last_id;
+    shl_dlist_link_tail(&con->sb.list, &line->list);
+    ++con->sb.count;
+    if (con->sb.pos == NULL)
+        con->sb.pos_num = con->sb.count;
 }

 /* Remove num lines from scroll back to current buffer */
 static void remove_from_sb(struct tsm_screen *con, unsigned int num)
 {
     struct line *tmp;
+    int i, copy_len;

     /* TODO: more sophisticated ageing */
     con->age = con->age_cnt;

-    if (!con->sb_max || !con->sb_count || !con->sb_last)
+    if (!con->sb.max || !con->sb.count || shl_dlist_empty(&con->sb.list))
         return;

-    if (num > con->sb_count)
-        num = con->sb_count;
+    if (num > con->sb.count)
+        num = con->sb.count;

     while (num--) {
-        tmp = con->sb_last;
-        con->sb_last = tmp->prev;
-
-        if (tmp->prev)
-            tmp->prev->next = NULL;
-        else
-            con->sb_first = NULL;
-        --con->sb_count;
-
-        tmp->next = NULL;
-        tmp->prev = NULL;
-        tmp->sb_id = 0;
-
-        if (con->sb_pos == tmp) {
-            con->sb_pos_num = 0;
-            con->sb_pos = NULL;
+        tmp = shl_dlist_last(&con->sb.list, struct line, list);
+        shl_dlist_unlink(&tmp->list);
+        --con->sb.count;
+
+        if (con->sb.pos == tmp) {
+            con->sb.pos_num = con->sb.count;
+            con->sb.pos = NULL;
         }
-        memcpy(con->lines[num], tmp, sizeof(*tmp));
-        free(tmp);
+        /*
+         * Copy the cells from the scrollback buffer to the line. scrollback 
buffer can have a different
+         * size as current lines, because resizing doesn't resize lines in 
scrollback buffer.
+         */
+        copy_len = shl_min(tmp->size, con->lines[num]->size);
+        memcpy(con->lines[num]->cells, tmp->cells, copy_len * sizeof(struct 
cell));
+        for (i = copy_len; i < con->size_x; i++)
+            screen_cell_init(con, &con->lines[num]->cells[i]);
+        con->lines[num]->age = con->age_cnt;
+
+        if (con->sel_active && con->sel_start.line == tmp)
+            con->sel_start.line = con->lines[num];
+        if (con->sel_active && con->sel_end.line == tmp)
+            con->sel_end.line = con->lines[num];
+
+        line_free(tmp);
     }
+    if (!con->sb.pos)
+        con->sb.pos_num = con->sb.count;
 }

 static void screen_scroll_up(struct tsm_screen *con, unsigned int num)
@@ -356,27 +332,6 @@

     memcpy(&con->lines[con->margin_top + (max - num)],
            cache, num * sizeof(struct line*));
-
-    if (con->sel_active) {
-        if (!con->sel_start.line && con->sel_start.y >= 0) {
-            con->sel_start.y -= num;
-            if (con->sel_start.y < 0) {
-                con->sel_start.line = con->sb_last;
-                while (con->sel_start.line && ++con->sel_start.y < 0)
-                    con->sel_start.line = con->sel_start.line->prev;
-                con->sel_start.y = SELECTION_TOP;
-            }
-        }
-        if (!con->sel_end.line && con->sel_end.y >= 0) {
-            con->sel_end.y -= num;
-            if (con->sel_end.y < 0) {
-                con->sel_end.line = con->sb_last;
-                while (con->sel_end.line && ++con->sel_end.y < 0)
-                    con->sel_end.line = con->sel_end.line->prev;
-                con->sel_end.y = SELECTION_TOP;
-            }
-        }
-    }
 }

 static void screen_scroll_down(struct tsm_screen *con, unsigned int num)
@@ -414,13 +369,6 @@

     memcpy(&con->lines[con->margin_top],
            cache, num * sizeof(struct line*));
-
-    if (con->sel_active) {
-        if (!con->sel_start.line && con->sel_start.y >= 0)
-            con->sel_start.y += num;
-        if (!con->sel_end.line && con->sel_end.y >= 0)
-            con->sel_end.y += num;
-    }
 }

 static void screen_write(struct tsm_screen *con, unsigned int x,
@@ -510,6 +458,12 @@
     return con->margin_top + y;
 }

+static void reset_scrollback_position(struct tsm_screen *con)
+{
+    con->sb.pos = NULL;
+    con->sb.pos_num = con->sb.count;
+}
+
 SHL_EXPORT
 int tsm_screen_new(struct tsm_screen **out, tsm_log_t log, void *log_data)
 {
@@ -533,6 +487,7 @@
     con->def_attr.fr = 255;
     con->def_attr.fg = 255;
     con->def_attr.fb = 255;
+    shl_dlist_init(&con->sb.list);

     ret = tsm_symbol_table_new(&con->sym_table);
     if (ret)
@@ -578,17 +533,16 @@
         return;

     llog_debug(con, "destroying screen");
+    tsm_screen_clear_sb(con);

     for (i = 0; i < con->line_num; ++i) {
         line_free(con->main_lines[i]);
         line_free(con->alt_lines[i]);
     }
-
     free(con->main_lines);
     free(con->alt_lines);
     free(con->tab_ruler);
     tsm_symbol_table_unref(con->sym_table);
-    tsm_screen_clear_sb(con);
     free(con);
 }

@@ -767,7 +721,7 @@
     /* scroll buffer if screen height shrinks */
     if (y < con->size_y) {
         diff = con->size_y - y;
-        if (!con->sb_last || (con->flags & TSM_SCREEN_ALTERNATE)) {
+        if (shl_dlist_empty(&con->sb.list) || (con->flags & 
TSM_SCREEN_ALTERNATE)) {
             /* If there is nothing in the scrollback buffer,
              * Only scroll up if the cursor would go off-screen */
             if (con->cursor_y >= y) {
@@ -784,8 +738,8 @@
         }
     } else if (y > con->size_y) {
         diff = y - con->size_y;
-        if (diff > con->sb_count)
-            diff = con->sb_count;
+        if (diff > con->sb.count)
+            diff = con->sb.count;
         /*
          * When increasing the terminal number of rows, we can move some
          * lines from the scrollback buffer to the main buffer.
@@ -835,7 +789,7 @@
     return 0;
 }

-/* set maximum scrollback buffer size */
+/* set maximum scrollback buffer size in number of lines*/
 SHL_EXPORT
 void tsm_screen_set_max_sb(struct tsm_screen *con,
                    unsigned int max)
@@ -845,45 +799,37 @@
     if (!con)
         return;

+    // Don't allow only one line in the scrollback buffer, this simplifies
+    // the code, and is not a useful usecase.
+    if (max == 1)
+        max = 2;
+
     screen_inc_age(con);
     /* TODO: more sophisticated ageing */
     con->age = con->age_cnt;

-    while (con->sb_count > max) {
-        line = con->sb_first;
-        con->sb_first = line->next;
-        if (line->next)
-            line->next->prev = NULL;
-        else
-            con->sb_last = NULL;
-        con->sb_count--;
+    while (con->sb.count > max) {
+        line = shl_dlist_first(&con->sb.list, struct line, list);
+        shl_dlist_unlink(&line->list);
+        --con->sb.count;

         /* We treat fixed/unfixed position the same here because we
          * remove lines from the TOP of the scrollback buffer. */
-        if (con->sb_pos == line)
-            con->sb_pos = con->sb_first;
+        if (con->sb.pos == line)
+            con->sb.pos = shl_dlist_first(&con->sb.list, struct line, list);

-        if (con->sel_active) {
-            if (con->sel_start.line == line) {
-                con->sel_start.line = NULL;
-                con->sel_start.y = SELECTION_TOP;
-            }
-            if (con->sel_end.line == line) {
-                con->sel_end.line = NULL;
-                con->sel_end.y = SELECTION_TOP;
-            }
-        }
+        clear_selection_on_line(con, line);
         line_free(line);
     }
-
-    con->sb_max = max;
+    con->sb.max = max;
 }

 /* clear scrollback buffer */
 SHL_EXPORT
 void tsm_screen_clear_sb(struct tsm_screen *con)
 {
-    struct line *iter, *tmp;
+    struct shl_dlist *iter, *safe;
+    struct line *tmp;

     if (!con)
         return;
@@ -892,33 +838,27 @@
     /* TODO: more sophisticated ageing */
     con->age = con->age_cnt;

-    for (iter = con->sb_first; iter; ) {
-        tmp = iter;
-        iter = iter->next;
-        line_free(tmp);
-    }
-
-    con->sb_first = NULL;
-    con->sb_last = NULL;
-    con->sb_count = 0;
-    con->sb_pos = NULL;
-    con->sb_pos_num = 0;
-
     if (con->sel_active) {
-        if (con->sel_start.line) {
+        if (con->sel_start.line && is_in_scrollback(&con->sel_start))
             con->sel_start.line = NULL;
-            con->sel_start.y = SELECTION_TOP;
-        }
-        if (con->sel_end.line) {
+        if (con->sel_end.line && is_in_scrollback(&con->sel_end))
             con->sel_end.line = NULL;
-            con->sel_end.y = SELECTION_TOP;
-        }
     }
+    shl_dlist_for_each_safe(iter, safe, &con->sb.list) {
+        tmp = shl_dlist_entry(iter, struct line, list);
+        shl_dlist_unlink(&tmp->list);
+        line_free(tmp);
+    }
+    con->sb.count = 0;
+    con->sb.pos = NULL;
+    con->sb.pos_num = 0;
 }

 SHL_EXPORT
 void tsm_screen_sb_up(struct tsm_screen *con, unsigned int num)
 {
+    struct line *prev;
+
     if (!con || !num)
         return;

@@ -926,18 +866,25 @@
     /* TODO: more sophisticated ageing */
     con->age = con->age_cnt;

+    if (shl_dlist_empty(&con->sb.list))
+        return;
+
     while (num--) {
-        if (con->sb_pos) {
-            if (!con->sb_pos->prev)
+        if (con->sb.pos) {
+            if (con->sb.pos_num == 0)
                 return;

-            con->sb_pos = con->sb_pos->prev;
-            --con->sb_pos_num;
-        } else if (!con->sb_last) {
-            return;
+            prev = shl_dlist_prev(con->sb.pos, &con->sb.list, list);
+            if (!prev) {
+                llog_error(con, "prev is NULL, con->sb.pos_num: %d con->sb.count: 
%d",
+                       con->sb.pos_num, con->sb.count);
+                return;
+            }
+            --con->sb.pos_num;
+            con->sb.pos = prev;
         } else {
-            con->sb_pos = con->sb_last;
-            con->sb_pos_num = con->sb_count - 1;
+            con->sb.pos = shl_dlist_last(&con->sb.list, struct line, list);
+            con->sb.pos_num = con->sb.count - 1;
         }
     }
 }
@@ -952,14 +899,12 @@
     /* TODO: more sophisticated ageing */
     con->age = con->age_cnt;

-    while (num--) {
-        if (con->sb_pos) {
-            con->sb_pos = con->sb_pos->next;
-            ++con->sb_pos_num;
-        }
-        else
-            return;
+    while (num-- && con->sb.pos && con->sb.pos_num < con->sb.count) {
+            con->sb.pos = shl_dlist_next(con->sb.pos, &con->sb.list, list);
+            ++con->sb.pos_num;
     }
+    if (con->sb.pos_num == con->sb.count)
+        con->sb.pos = NULL;
 }

 SHL_EXPORT
@@ -985,15 +930,15 @@
 SHL_EXPORT
 void tsm_screen_sb_reset(struct tsm_screen *con)
 {
-    if (!con || !con->sb_pos)
+    if (!con || !con->sb.pos)
         return;

     screen_inc_age(con);
     /* TODO: more sophisticated ageing */
     con->age = con->age_cnt;

-    con->sb_pos = NULL;
-    con->sb_pos_num = con->sb_count;
+    con->sb.pos = NULL;
+    con->sb.pos_num = con->sb.count;
 }

 unsigned int tsm_screen_sb_get_line_count(struct tsm_screen *con)
@@ -1002,7 +947,7 @@
         return 0;
     }

-    return con->sb_count;
+    return con->sb.count;
 }

 unsigned int tsm_screen_sb_get_line_pos(struct tsm_screen *con)
@@ -1011,7 +956,7 @@
         return 0;
     }

-    return con->sb_pos_num;
+    return con->sb.pos_num;
 }

 SHL_EXPORT
@@ -1021,6 +966,8 @@
     if (!con || !attr)
         return;
     memcpy(&con->def_attr, attr, sizeof(*attr));
+    if (!(con->flags & TSM_SCREEN_ALTERNATE))
+        memcpy(&con->def_attr_main, attr, sizeof(*attr));
 }

 SHL_EXPORT
@@ -1045,6 +992,8 @@
         else
             con->tab_ruler[i] = false;
     }
+    tsm_screen_selection_reset(con);
+    reset_scrollback_position(con);
 }

 SHL_EXPORT
@@ -1064,6 +1013,8 @@
     if (!(old & TSM_SCREEN_ALTERNATE) && (flags & TSM_SCREEN_ALTERNATE)) {
         con->age = con->age_cnt;
         con->lines = con->alt_lines;
+        tsm_screen_selection_reset(con);
+        reset_scrollback_position(con);

         /* save attributes of main screen when we switch to alt screen */
         memcpy(&con->def_attr_main, &con->def_attr, sizeof(con->def_attr));
@@ -1096,6 +1047,8 @@
     if ((old & TSM_SCREEN_ALTERNATE) && (flags & TSM_SCREEN_ALTERNATE)) {
         con->age = con->age_cnt;
         con->lines = con->main_lines;
+        tsm_screen_selection_reset(con);
+        reset_scrollback_position(con);
     }

     if ((old & TSM_SCREEN_HIDE_CURSOR) &&
diff -Nru libtsm-4.4.3/src/tsm/tsm-selection.c 
libtsm-4.5.0/src/tsm/tsm-selection.c
--- libtsm-4.4.3/src/tsm/tsm-selection.c    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/src/tsm/tsm-selection.c    2026-04-21 05:27:37.000000000 -0400
@@ -58,27 +58,30 @@
 #include "libtsm.h"
 #include "libtsm-int.h"
 #include "shl-llog.h"
+#include "shl_dlist.h"

 #define LLOG_SUBSYSTEM "tsm-selection"

 static void selection_set(struct tsm_screen *con, struct selection_pos *sel,
               unsigned int x, unsigned int y)
 {
-    struct line *pos;
+    struct line *line;

-    sel->line = NULL;
-    pos = con->sb_pos;
+    sel->x = x;

-    while (y && pos) {
-        --y;
-        pos = pos->next;
+    if (!con->sb.pos) {
+        sel->line = con->lines[y];
+        return;
     }
-
-    if (pos)
-        sel->line = pos;
-
-    sel->x = x;
-    sel->y = y;
+    if (con->sb.pos_num + y >= con->sb.count) {
+        y -= con->sb.count - con->sb.pos_num;
+        sel->line = con->lines[y];
+        return;
+    }
+    line = con->sb.pos;
+    while (y--)
+        line = shl_dlist_next(line, &con->sb.list, list);
+    sel->line = line;
 }

 static void word_select(struct tsm_screen *con,
@@ -90,10 +93,7 @@

     selection_set(con, &con->sel_start, posx, posy);

-    if (con->sel_start.line)
-        line = con->sel_start.line;
-    else
-         line = con->lines[con->sel_start.y];
+    line = con->sel_start.line;

     if (!line || line->cells[posx].ch == ' ')
         return;
@@ -115,7 +115,8 @@
         }
     }
     con->sel_start.x = start;
-    selection_set(con, &con->sel_end, end, posy);
+    con->sel_end.x = end;
+    con->sel_end.line = line;
     con->sel_active = true;
 }

@@ -130,110 +131,57 @@
     con->age = con->age_cnt;

     con->sel_active = false;
-}
-
-SHL_EXPORT
-void tsm_screen_selection_start(struct tsm_screen *con,
-                unsigned int posx,
-                unsigned int posy)
-{
-    if (!con)
-        return;
-
-    screen_inc_age(con);
-    /* TODO: more sophisticated ageing */
-    con->age = con->age_cnt;
-
-    con->sel_active = true;
-    selection_set(con, &con->sel_start, posx, posy);
-    memcpy(&con->sel_end, &con->sel_start, sizeof(con->sel_end));
-}
-
-SHL_EXPORT
-void tsm_screen_selection_target(struct tsm_screen *con,
-                 unsigned int posx,
-                 unsigned int posy)
-{
-    if (!con || !con->sel_active)
-        return;
-
-    screen_inc_age(con);
-    /* TODO: more sophisticated ageing */
-    con->age = con->age_cnt;
-
-    selection_set(con, &con->sel_end, posx, posy);
-}
-
-SHL_EXPORT
-void tsm_screen_selection_word(struct tsm_screen *con,
-                   unsigned int posx,
-                   unsigned int posy)
-{
-    if (!con)
-        return;
-
-    screen_inc_age(con);
-    /* TODO: more sophisticated ageing */
-    con->age = con->age_cnt;
-
-    word_select(con, posx, posy);
+    con->sel_start.line = NULL;
+    con->sel_end.line = NULL;
 }

 /* calculates the line length from the beginning to the last non zero 
character */
 static unsigned int calc_line_len(struct line *line)
 {
-    unsigned int line_len = 0;
     int i;

-    for (i = 0; i < line->size; i++) {
-        if (line->cells[i].ch != 0) {
-            line_len = i + 1;
-        }
-    }
-
-    return line_len;
+    for (i = line->size - 1; i >= 0; i--)
+        if (line->cells[i].ch != 0)
+            return i + 1;
+    return 0;
 }

 /* TODO: tsm_ucs4_to_utf8 expects UCS4 characters, but a cell contains a
  * tsm-symbol (which can contain multiple UCS4 chars). Fix this when 
introducing
  * support for combining characters. */
-static unsigned int copy_line(struct line *line, char *buf,
-                  unsigned int start, unsigned int len)
+static unsigned int copy_line(struct tsm_screen *con, struct line *line, char 
*buf)
 {
-    unsigned int i, end;
+    unsigned int i, start, end;
     char *pos = buf;
     int line_len;

     line_len = calc_line_len(line);
-    if (start > line_len) {
-        return 0;
-    }
+    start = (con->sel_start.line == line) ? con->sel_start.x : 0;
+    end = (con->sel_end.line == line) ? con->sel_end.x + 1 : con->size_x;

-    end = start + len;
+    if (start > line_len)
+        return 0;

-    if (end > line_len) {
+    if (end > line_len)
         end = line_len;
-    }

-    for (i = start; i < line->size && i < end; ++i) {
-        if (i < line->size || !line->cells[i].ch)
+    for (i = start; i < end; i++) {
+        if (line->cells[i].ch)
             pos += tsm_ucs4_to_utf8(line->cells[i].ch, pos);
         else
             pos += tsm_ucs4_to_utf8(' ', pos);
     }
-
     pos += tsm_ucs4_to_utf8('\n', pos);
-
     return pos - buf;
 }

-static void swap_selections(struct selection_pos **a, struct selection_pos **b)
+static void swap_selections(struct tsm_screen *con)
 {
-    struct selection_pos *c;
+    struct selection_pos c;

-    c  = *a;
-    *a = *b;
-    *b = c;
+    c = con->sel_start;
+    con->sel_start = con->sel_end;
+    con->sel_end = c;
 }

 /*
@@ -241,158 +189,141 @@
  *
  * Start must always point to the top left and end to the bottom right cell
  */
-static void norm_selection(struct tsm_screen *con, struct selection_pos 
**start, struct selection_pos **end)
+static void norm_selection(struct tsm_screen *con)
 {
-    struct line *iter;
+    int i;
+    struct selection_pos *start, *end;

-    if ((*end)->line == NULL && (*end)->y == SELECTION_TOP) {
-        swap_selections(start, end);
+    start = &con->sel_start;
+    end = &con->sel_end;

+    if (start->line == end->line) {
+        if (con->sel_start.x > con->sel_end.x)
+            swap_selections(con);
         return;
     }

-    if ((*start)->line && (*end)->line) {
-        /* single line selection */
-        if ((*start)->line == (*end)->line) {
-            if ((*start)->x > (*end)->x) {
-                swap_selections(start, end);
-            }
-
-            return;
-        }
-
-        /*
-         * multi line selection
-         *
-         * search from end->line to con->sb_last
-         * if we find start->line on the way we
-         * need to change start and end
-        */
-        iter = (*end)->line;
-        while (iter && iter != con->sb_last) {
-            if (iter == (*start)->line) {
-                swap_selections(start, end);
-            }
-
-            iter = iter->next;
-        }
-
+    if (is_in_scrollback(&con->sel_start) != is_in_scrollback(&con->sel_end)) {
+        if (is_in_scrollback(&con->sel_end))
+            swap_selections(con);
         return;
     }

-    /* end is in scroll back buffer and start on screen */
-    if (!(*start)->line && (*end)->line) {
-        swap_selections(start, end);
+    if (is_in_scrollback(&con->sel_start) && is_in_scrollback(&con->sel_end)) {
+        if (con->sel_start.line->sb_id > con->sel_end.line->sb_id)
+            swap_selections(con);
         return;
     }

-    /* reorder one-line selection if selection was created right to left */
-    if ((*start)->y == (*end)->y) {
-        if ((*start)->x > (*end)->x) {
-            swap_selections(start, end);
+    /* so both are not in scroll back buffer and can't be equal */
+    for (i = 0; i < con->size_y; i++) {
+        if (con->lines[i] == con->sel_end.line) {
+            swap_selections(con);
+            return;
         }
+        if (con->lines[i] == con->sel_start.line)
+            return;
+    }
+}

+SHL_EXPORT
+void tsm_screen_selection_start(struct tsm_screen *con,
+                unsigned int posx,
+                unsigned int posy)
+{
+    if (!con || posx >= con->size_x || posy >= con->size_y)
         return;
-    }

-    /* reorder multi-line selection if selection was created bottom to top */
-    if ((*start)->y > (*end)->y) {
-        swap_selections(start, end);
-    }
+    screen_inc_age(con);
+    /* TODO: more sophisticated ageing */
+    con->age = con->age_cnt;
+
+    con->sel_active = true;
+    selection_set(con, &con->sel_start, posx, posy);
+    memcpy(&con->sel_end, &con->sel_start, sizeof(con->sel_end));
 }

-/*
- * Counts the lines a normalized selection selects on the scroll back buffer
- *
- * Does not count the lines selected on the screen
- */
-static int selection_count_lines_sb(struct tsm_screen *con, struct 
selection_pos *start, struct selection_pos *end)
+SHL_EXPORT
+void tsm_screen_selection_target(struct tsm_screen *con,
+                 unsigned int posx,
+                 unsigned int posy)
 {
-    struct line *iter;
-    int count = 0;
+    if (!con || !con->sel_active || posx >= con->size_x || posy >= con->size_y)
+        return;

-    /* Single line selection */
-    if (start->line && (start->line == end->line)) {
-        return 1;
-    }
+    screen_inc_age(con);
+    /* TODO: more sophisticated ageing */
+    con->age = con->age_cnt;

-    iter = start->line;
-    while (iter) {
-        count++;
+    selection_set(con, &con->sel_end, posx, posy);
+    /* always normalize the selection */
+    norm_selection(con);
+}

-        if (iter == con->sb_last) {
-            break;
-        }
+SHL_EXPORT
+void tsm_screen_selection_word(struct tsm_screen *con,
+                   unsigned int posx,
+                   unsigned int posy)
+{
+    if (!con || posx >= con->size_x || posy >= con->size_y)
+        return;

-        iter = iter->next;
-    }
+    screen_inc_age(con);
+    /* TODO: more sophisticated ageing */
+    con->age = con->age_cnt;

-    return count;
+    word_select(con, posx, posy);
 }

 /*
- * Counts the lines a normalized selection selects on the screen
+ * Get the index of a line in the screen
  *
- * Does not count the lines selected in the scroll back buffer
+ * If the line is in the scroll back buffer, return 0
+ * Otherwise, return the index of the line in the screen
  */
-static int selection_count_lines(struct selection_pos *start, struct 
selection_pos *end)
+static unsigned int get_line_index(struct tsm_screen *con, struct line *line)
 {
-    /* Selection only spans lines of the scroll back buffer */
-    if (start->line && end->line) {
+    unsigned int i = 0;
+
+    if (line->sb_id)
         return 0;
-    }

-    return end->y - start->y + 1;
+    for (i = 0; i < con->size_y; i++) {
+        if (con->lines[i] == line)
+            return i;
+    }
+    return 0;
 }

-/*
- * Calculate the number of selected cells in a line
- */
-static int calc_selection_line_len_sb(struct tsm_screen *con, struct selection_pos *start, struct selection_pos *end, struct line *line)
+static struct line *get_next_line(struct tsm_screen *con, struct line *line, 
unsigned int *index)
 {
-    /* one-line selection */
-    if (start->line == end->line) {
-        return end->x - start->x + 1;
-    }
-
-    /* first line of a multi-line selection */
-    if (line == start->line) {
-        return con->size_x - start->x;
-    }
+    struct line *next;

-    /* last line of a multi-line selection */
-    if (line == end->line) {
-        return end->x + 1;
+    if (line->sb_id) {
+        next = shl_dlist_next(line, &con->sb.list, list);
+        if (next)
+            return next;
+        *index = 0;
+        return con->lines[0];
+    } else if (*index < con->size_y - 1) {
+        (*index)++;
+        return con->lines[*index];
     }
-
-    /* every other selection */
-    return con->size_x;
+    return NULL;
 }

-/*
- * Calculate the number of selected cells in a line
- */
-static int calc_selection_line_len(struct tsm_screen *con, struct selection_pos *start, struct selection_pos *end, int line_num)
+static int selection_count_lines(struct tsm_screen *con, struct selection_pos 
*start, struct selection_pos *end)
 {
-    if (!start->line) {
-        /* one-line selection */
-        if (start->y == end->y) {
-            return end->x - start->x + 1;
-        }
-
-        /* first line of a multi-line selection */
-        if (line_num == start->y) {
-            return con->size_x - start->x;
-        }
-    }
+    int count = 1;
+    unsigned int index = get_line_index(con, start->line);
+    struct line *iter;

-    /* last line of a multi-line selection */
-    if (line_num == end->y) {
-        return end->x + 1;
+    iter = start->line;
+    while (iter && iter != end->line) {
+        count++;
+        iter = get_next_line(con, iter, &index);
     }
-
-    /* every other selection */
-    return con->size_x;
+    return count;
 }

 /*
@@ -404,69 +335,26 @@
     return con->size_x * num_lines * 4 + 1;
 }

-/*
- * Copy all selected lines from the scroll back buffer
- */
-static int copy_lines_sb(struct tsm_screen *con, struct selection_pos *start, struct selection_pos *end, char *buf, int pos)
+static int copy_lines(struct tsm_screen *con, struct selection_pos *start, 
struct selection_pos *end, char *buf, int pos)
 {
+    unsigned int index = get_line_index(con, start->line);
     struct line *iter;
-    int line_x, line_len;
-
-    if (!start->line) {
-        return pos;
-    }

     iter = start->line;
     while (iter) {
-        line_x = 0;
-        if (iter == start->line) {
-            line_x = start->x;
-        }
-
-        line_len = calc_selection_line_len_sb(con, start, end, iter);
-        pos += copy_line(iter, &(buf[pos]), line_x, line_len);
-
-        if (iter == con->sb_last || iter == end->line) {
+        pos += copy_line(con, iter, &(buf[pos]));
+        if (iter == end->line)
             break;
-        }
-
-        iter = iter->next;
+        iter = get_next_line(con, iter, &index);
     }
-
-    return pos;
-}
-
-/*
- * Copy all selected lines from the regular screen
- */
-static int copy_lines(struct tsm_screen *con, struct selection_pos *start, 
struct selection_pos *end, char *buf, int pos)
-{
-    int line_len, line_x, i;
-
-    /* selection is scroll back buffer only */
-    if (end->line) {
-        return pos;
-    }
-
-    for (i = start->y; i <= end->y; i++) {
-        line_len = calc_selection_line_len(con, start, end, i);
-
-        line_x = 0;
-        if (!start->line && i == start->y) {
-            line_x = start->x;
-        }
-
-        pos += copy_line(con->lines[i], &(buf[pos]), line_x, line_len);
-    }
-
     return pos;
 }

 SHL_EXPORT
 int tsm_screen_selection_copy(struct tsm_screen *con, char **out)
 {
-    struct selection_pos *start, *end;
-    struct selection_pos start_copy, end_copy;
+    struct selection_pos *start = &con->sel_start;
+    struct selection_pos *end = &con->sel_end;
     int buf_size = 0;
     int pos = 0;
     int total_lines;
@@ -479,36 +367,21 @@
         return -ENOENT;
     }

-    /*
-     * copy the selection start and end so we can modify it without affecting
-     * the screen in any way
-     */
-    memcpy(&start_copy, &con->sel_start, sizeof(con->sel_start));
-    memcpy(&end_copy, &con->sel_end, sizeof(con->sel_end));
-    start = &start_copy;
-    end   = &end_copy;
-
     /* invalid selection */
-    if (start->y == SELECTION_TOP && start->line == NULL &&
-        end->y == SELECTION_TOP && end->line == NULL) {
+    if (start->line == NULL && end->line == NULL) {
         *out = strdup("");
         return 0;
     }

-    norm_selection(con, &start, &end);
-
-    if (start->line == NULL && start->y == SELECTION_TOP) {
-        if (con->sb_first != NULL) {
-            start->line = con->sb_first;
-            start->x = 0;
-        } else {
-            start->y = 0;
-            start->x = 0;
-        }
+    if (start->line == NULL) {
+        if (!shl_dlist_empty(&con->sb.list))
+            start->line = shl_dlist_first(&con->sb.list, struct line, list);
+        else
+            start->line = con->lines[0];
+        start->x = 0;
     }

-    total_lines =  selection_count_lines_sb(con, start, end);
-    total_lines += selection_count_lines(start, end);
+    total_lines =  selection_count_lines(con, start, end);
     buf_size = calc_line_copy_buffer(con, total_lines);

     *out = calloc(buf_size, 1);
@@ -516,7 +389,6 @@
         return -ENOMEM;
     }

-    pos = copy_lines_sb(con, start, end, *out, pos);
     pos = copy_lines(con, start, end, *out, pos);

     /* remove last line break */
diff -Nru libtsm-4.4.3/src/tsm/tsm-vte.c libtsm-4.5.0/src/tsm/tsm-vte.c
--- libtsm-4.4.3/src/tsm/tsm-vte.c    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/src/tsm/tsm-vte.c    2026-04-21 05:27:37.000000000 -0400
@@ -178,6 +178,13 @@
     unsigned int mouse_last_row;
     bool bracketed_paste;

+    tsm_vte_bell_cb bell_cb;
+    void *bell_data;
+
+    tsm_vte_led_cb led_cb;
+    void *led_data;
+    unsigned int led_state;
+
     uint8_t (*custom_palette_storage)[3];
     uint8_t (*palette)[3];
     struct tsm_screen_attr def_attr;
@@ -576,6 +583,26 @@
     vte->mouse_data = mouse_data;
 }

+SHL_EXPORT
+void tsm_vte_set_bell_cb(struct tsm_vte *vte, tsm_vte_bell_cb bell_cb, void 
*bell_data)
+{
+    if (!vte)
+        return;
+
+    vte->bell_cb = bell_cb;
+    vte->bell_data = bell_data;
+}
+
+SHL_EXPORT
+void tsm_vte_set_led_cb(struct tsm_vte *vte, tsm_vte_led_cb led_cb, void 
*led_data)
+{
+    if (!vte)
+        return;
+
+    vte->led_cb = led_cb;
+    vte->led_data = led_data;
+}
+
 static int vte_update_palette(struct tsm_vte *vte)
 {
     vte->palette = get_palette(vte);
@@ -880,11 +907,8 @@
         vte_write(vte, "\x06", 1);
         break;
     case 0x07: /* BEL */
-        /* Sound bell tone */
-        /* TODO: I always considered this annying, however, we
-         * should at least provide some way to enable it if the
-         * user *really* wants it.
-         */
+        if (vte->bell_cb)
+            vte->bell_cb(vte, vte->bell_data);
         break;
     case 0x08: /* BS */
         /* Move cursor one position left */
@@ -2100,6 +2124,22 @@
         num = vte->csi_argv[0];
         tsm_screen_repeat_char(vte->con, num);
         break;
+    case 'q': /* DECLL - Load LEDs */
+        num = vte->csi_argv[0];
+        if (num <= 0) {
+            vte->led_state = 0;
+        } else if (num == 1) {
+            vte->led_state |= TSM_VTE_LED_SCROLL_LOCK;
+        } else if (num == 2) {
+            vte->led_state |= TSM_VTE_LED_NUM_LOCK;
+        } else if (num == 3) {
+            vte->led_state |= TSM_VTE_LED_CAPS_LOCK;
+        } else {
+            break;
+        }
+        if (vte->led_cb)
+            vte->led_cb(vte, vte->led_state, vte->led_data);
+        break;
     default:
         llog_debug(vte, "unhandled CSI sequence %c", data);
     }
diff -Nru libtsm-4.4.3/test/test_screen.c libtsm-4.5.0/test/test_screen.c
--- libtsm-4.4.3/test/test_screen.c    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/test/test_screen.c    2026-04-21 05:27:37.000000000 -0400
@@ -257,6 +257,34 @@
     ck_assert_int_eq(tsm_screen_sb_get_line_count(screen), 3);
     ck_assert_int_eq(tsm_screen_sb_get_line_pos(screen), 3);

+    tsm_screen_newline(screen);
+    ck_assert_int_eq(tsm_screen_sb_get_line_count(screen), 4);
+    ck_assert_int_eq(tsm_screen_sb_get_line_pos(screen), 4);
+
+    tsm_screen_newline(screen);
+    ck_assert_int_eq(tsm_screen_sb_get_line_count(screen), 5);
+    ck_assert_int_eq(tsm_screen_sb_get_line_pos(screen), 5);
+
+    tsm_screen_sb_up(screen, 2);
+    ck_assert_int_eq(tsm_screen_sb_get_line_count(screen), 5);
+    ck_assert_int_eq(tsm_screen_sb_get_line_pos(screen), 3);
+
+    tsm_screen_newline(screen);
+    ck_assert_int_eq(tsm_screen_sb_get_line_count(screen), 5);
+    ck_assert_int_eq(tsm_screen_sb_get_line_pos(screen), 2);
+
+    r = tsm_screen_resize(screen, 5, 3);
+    ck_assert_int_eq(r, 0);
+
+    ck_assert_int_eq(tsm_screen_sb_get_line_count(screen), 5);
+    ck_assert_int_eq(tsm_screen_sb_get_line_pos(screen), 0);
+
+    r = tsm_screen_resize(screen, 5, 5);
+    ck_assert_int_eq(r, 0);
+
+    ck_assert_int_eq(tsm_screen_sb_get_line_count(screen), 3);
+    ck_assert_int_eq(tsm_screen_sb_get_line_pos(screen), 0);
+
     tsm_screen_unref(screen);
     screen = NULL;
 }
diff -Nru libtsm-4.4.3/test/test_selection.c libtsm-4.5.0/test/test_selection.c
--- libtsm-4.4.3/test/test_selection.c    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/test/test_selection.c    2026-04-21 05:27:37.000000000 -0400
@@ -203,9 +203,7 @@
     tsm_screen_selection_target(screen, 14, 39);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 39);
     ck_assert_int_eq(screen->sel_end.x, 14);
-    ck_assert_int_eq(screen->sel_end.y, 39);

     /* force the selected text to scroll up */
     tsm_screen_newline(screen);
@@ -217,9 +215,7 @@
     tsm_screen_newline(screen);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 32);
     ck_assert_int_eq(screen->sel_end.x, 14);
-    ck_assert_int_eq(screen->sel_end.y, 32);

     r = tsm_screen_selection_copy(screen, &str);
     ck_assert_ptr_ne(NULL, str);
@@ -267,7 +263,6 @@
     str = NULL;

     /* Select "This is a copy test\nfor a selection" from top left and to 
bottom right copy it */
-    tsm_screen_reset(screen);
     tsm_screen_selection_start(screen, 0, 2);
     tsm_screen_selection_target(screen, 14, 3);

@@ -278,7 +273,6 @@
     str = NULL;

     /* Select all text excluding the first 3 spaces and the trailing '-' chars from bottom right to top left and copy it */
-    tsm_screen_reset(screen);
     tsm_screen_selection_start(screen, 41, 4);
     tsm_screen_selection_target(screen, 3, 1);

@@ -321,9 +315,7 @@
     tsm_screen_selection_target(screen, 5, 39);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 37);
     ck_assert_int_eq(screen->sel_end.x, 5);
-    ck_assert_int_eq(screen->sel_end.y, 39);

     /* force the selected text to scroll up */
     tsm_screen_newline(screen);
@@ -335,9 +327,7 @@
     tsm_screen_newline(screen);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 30);
     ck_assert_int_eq(screen->sel_end.x, 5);
-    ck_assert_int_eq(screen->sel_end.y, 32);

     r = tsm_screen_selection_copy(screen, &str);
     ck_assert_ptr_ne(NULL, str);
@@ -462,9 +452,7 @@
     tsm_screen_selection_target(screen, 14, 0);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 0);
     ck_assert_int_eq(screen->sel_end.x, 14);
-    ck_assert_int_eq(screen->sel_end.y, 0);

     /* force the selected text to scroll up */
     for (int i = 0; i < 40; i++) {
@@ -472,10 +460,8 @@
     }

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, -1);
     ck_assert_ptr_ne(screen->sel_start.line, NULL);
     ck_assert_int_eq(screen->sel_end.x, 14);
-    ck_assert_int_eq(screen->sel_end.y, -1);
     ck_assert_ptr_ne(screen->sel_end.line, NULL);

     r = tsm_screen_selection_copy(screen, &str);
@@ -496,10 +482,8 @@
     tsm_screen_selection_target(screen, 14, 0);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 0);
     ck_assert_ptr_ne(screen->sel_start.line, NULL);
     ck_assert_int_eq(screen->sel_end.x, 14);
-    ck_assert_int_eq(screen->sel_end.y, 0);
     ck_assert_ptr_ne(screen->sel_end.line, NULL);

     tsm_screen_newline(screen);
@@ -507,10 +491,8 @@
     tsm_screen_newline(screen);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 0);
     ck_assert_ptr_ne(screen->sel_start.line, NULL);
     ck_assert_int_eq(screen->sel_end.x, 14);
-    ck_assert_int_eq(screen->sel_end.y, 0);
     ck_assert_ptr_ne(screen->sel_end.line, NULL);

     r = tsm_screen_selection_copy(screen, &str);
@@ -549,26 +531,16 @@
     tsm_screen_selection_target(screen, 14, 0);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 0);
     ck_assert_int_eq(screen->sel_end.x, 14);
-    ck_assert_int_eq(screen->sel_end.y, 0);

     /* force the selected text to scroll up */
     for (i = 0; i < 40; i++) {
         tsm_screen_newline(screen);
     }

-    /*
-     * sel_start.y == -1, sel_start.line == NULL
-     * sel_end.y == -1, sel_end.line == NULL
-     *
-     * => Invalid selection
-     */
     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, -1);
     ck_assert_ptr_eq(screen->sel_start.line, NULL);
     ck_assert_int_eq(screen->sel_end.x, 14);
-    ck_assert_int_eq(screen->sel_end.y, -1);
     ck_assert_ptr_eq(screen->sel_end.line, NULL);

     r = tsm_screen_selection_copy(screen, &str);
@@ -630,7 +602,6 @@
     str = NULL;

     /* Select "This is a copy test\nfor a selection" from top left and to 
bottom right copy it */
-    tsm_screen_reset(screen);
     tsm_screen_selection_start(screen, 0, 2);
     tsm_screen_selection_target(screen, 14, 3);

@@ -641,10 +612,12 @@
     str = NULL;

     /* Select all text excluding the first 3 spaces and the trailing '-' chars from bottom right to top left and copy it */
-    tsm_screen_reset(screen);
     tsm_screen_selection_start(screen, 41, 4);
     tsm_screen_selection_target(screen, 3, 1);

+    ck_assert_int_eq(screen->sel_start.x, 3);
+    ck_assert_int_eq(screen->sel_end.x, 41);
+
     r = tsm_screen_selection_copy(screen, &str);
     ck_assert_ptr_ne(NULL, str);
     ck_assert_str_eq("Hello World!\nThis is a copy test\nfor a selection with multiple lines.\nAll of them are on screen (not in the sb).", str);
@@ -652,7 +625,6 @@
     str = NULL;

     /* Select from scroll back buffer and the screen from top left to bottom 
right and copy it */
-    tsm_screen_reset(screen);
     tsm_screen_selection_start(screen, 0, 4);
     tsm_screen_selection_target(screen, 18, 6);

@@ -663,10 +635,12 @@
     str = NULL;

     /* Select from scroll back buffer and the screen from bottom right to top 
left and copy it */
-    tsm_screen_reset(screen);
     tsm_screen_selection_start(screen, 18, 6);
     tsm_screen_selection_target(screen, 0, 4);

+    ck_assert_int_eq(screen->sel_start.x, 0);
+    ck_assert_int_eq(screen->sel_end.x, 18);
+
     r = tsm_screen_selection_copy(screen, &str);
     ck_assert_ptr_ne(NULL, str);
     ck_assert_str_eq("All of them are on screen (not in the sb).------\nText not in 
SB\nMore Text not in SB", str);
@@ -674,7 +648,6 @@
     str = NULL;

     /* Select from scroll back buffer and the screen from bottom right to top 
left and copy it */
-    tsm_screen_reset(screen);
     tsm_screen_selection_start(screen, 8, 6);
     tsm_screen_selection_target(screen, 7, 4);

@@ -719,9 +692,7 @@
     tsm_screen_selection_target(screen, 5, 2);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 0);
     ck_assert_int_eq(screen->sel_end.x, 5);
-    ck_assert_int_eq(screen->sel_end.y, 2);

     /* force the selected text to scroll into the sb */
     for (i = 0; i < 40; i++) {
@@ -729,10 +700,8 @@
     }

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, -1);
     ck_assert_ptr_ne(screen->sel_start.line, NULL);
     ck_assert_int_eq(screen->sel_end.x, 5);
-    ck_assert_int_eq(screen->sel_end.y, -1);
     ck_assert_ptr_ne(screen->sel_end.line, NULL);
     ck_assert_ptr_ne(screen->sel_start.line, screen->sel_end.line);

@@ -754,19 +723,15 @@
     tsm_screen_selection_target(screen, 5, 2);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 0);
     ck_assert_int_eq(screen->sel_end.x, 5);
-    ck_assert_int_eq(screen->sel_end.y, 0);

     tsm_screen_newline(screen);
     tsm_screen_newline(screen);
     tsm_screen_newline(screen);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 0);
     ck_assert_ptr_ne(screen->sel_start.line, NULL);
     ck_assert_int_eq(screen->sel_end.x, 5);
-    ck_assert_int_eq(screen->sel_end.y, 0);
     ck_assert_ptr_ne(screen->sel_end.line, NULL);
     ck_assert_ptr_ne(screen->sel_start.line, screen->sel_end.line);

@@ -810,9 +775,7 @@
     tsm_screen_selection_target(screen, 5, 2);

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, 0);
     ck_assert_int_eq(screen->sel_end.x, 5);
-    ck_assert_int_eq(screen->sel_end.y, 2);

     /* force the selected text to scroll up */
     for (i = 0; i < 39; i++) {
@@ -820,11 +783,9 @@
     }

     ck_assert_int_eq(screen->sel_start.x, 3);
-    ck_assert_int_eq(screen->sel_start.y, -1);
-    ck_assert_ptr_eq(screen->sel_start.line, NULL);
+    ck_assert(!is_in_scrollback(&screen->sel_start));
     ck_assert_int_eq(screen->sel_end.x, 5);
-    ck_assert_int_eq(screen->sel_end.y, 0);
-    ck_assert_ptr_eq(screen->sel_end.line, NULL);
+    ck_assert(!is_in_scrollback(&screen->sel_end));

     r = tsm_screen_selection_copy(screen, &str);
     ck_assert_ptr_ne(NULL, str);
@@ -837,6 +798,96 @@
 }
 END_TEST

+static void check_sb_pos(struct tsm_screen *screen)
+{
+    struct line *line = shl_dlist_first(&screen->sb.list, struct line, list);
+    int count = 0;
+
+    if (!screen->sb.pos) {
+        ck_assert_int_eq(screen->sb.pos_num, screen->sb.count);
+        return;
+    }
+
+    ck_assert_int_le(screen->sb.pos_num, screen->sb.count);
+
+    while (line && line != screen->sb.pos) {
+        count++;
+        line = shl_dlist_next(line, &screen->sb.list, list);
+    }
+
+    ck_assert_int_eq(count, screen->sb.pos_num);
+}
+
+static void write_random_string(struct tsm_screen *screen, int count)
+{
+    char str[201];
+    int len = rand() % 100;
+    int i, c;
+
+    for (c = 0; c < count; c++) {
+        for (i = 0; i < len; i++)
+            str[i] = ' ' + rand() % 95;
+        str[len] = '\0';
+        write_string(screen, str);
+        tsm_screen_newline(screen);
+    }
+}
+
+START_TEST(test_screen_robustness)
+{
+    struct tsm_screen *screen;
+    int sb_size = 50;
+    int size_x, size_y;
+    int r, i, j;
+    char *str = NULL;
+
+
+    srand(0x12345678);
+
+    r = tsm_screen_new(&screen, NULL, NULL);
+    ck_assert_int_eq(r, 0);
+
+    r = tsm_screen_resize(screen, 80, 40);
+    ck_assert_int_eq(r, 0);
+
+    tsm_screen_set_max_sb(screen, sb_size);
+
+    write_random_string(screen, 60);
+
+    check_sb_pos(screen);
+
+    for (i = 0; i < 100; i++) {
+        size_x =  1 + rand() % 100;
+        size_y =  1 + rand() % 100;
+        r = tsm_screen_resize(screen, size_x, size_y);
+        ck_assert_int_eq(r, 0);
+        check_sb_pos(screen);
+        tsm_screen_sb_up(screen, rand() % sb_size);
+        check_sb_pos(screen);
+        tsm_screen_sb_down(screen, rand() % sb_size);
+        check_sb_pos(screen);
+        tsm_screen_selection_start(screen, rand() % size_x, rand() % size_y);
+        tsm_screen_selection_target(screen, rand() % size_x, rand() % size_y);
+        for (j = 0; j < 20; j++) {
+            r = tsm_screen_selection_copy(screen, &str);
+            ck_assert_int_ge(r, 0);
+            if (str) {
+                free(str);
+                str = NULL;
+            }
+            tsm_screen_sb_up(screen, sb_size );
+            check_sb_pos(screen);
+            tsm_screen_sb_down(screen, rand() % sb_size);
+            check_sb_pos(screen);
+        }
+        write_random_string(screen, rand() % 100);
+    }
+
+    tsm_screen_unref(screen);
+    screen = NULL;
+}
+END_TEST
+
 TEST_DEFINE_CASE(misc)
     TEST(test_screen_copy_incomplete)
     TEST(test_screen_copy_one_cell)
@@ -850,6 +901,7 @@
     TEST(test_screen_copy_lines_sb)
     TEST(test_screen_copy_lines_sb_scrolled)
     TEST(test_screen_copy_lines_sb_scrolled_cut_off)
+    TEST(test_screen_robustness)
 TEST_END_CASE

 TEST_DEFINE(
diff -Nru libtsm-4.4.3/test/test_vte.c libtsm-4.5.0/test/test_vte.c
--- libtsm-4.4.3/test/test_vte.c    2026-03-20 09:44:52.000000000 -0400
+++ libtsm-4.5.0/test/test_vte.c    2026-04-21 05:27:37.000000000 -0400
@@ -228,6 +228,12 @@
     input = "\e]11;?;12;?\x07";
     strcpy(expected_output, "\e]11;rgb:1111/2323/3535\x07");
     checked_vte_input(vte, input, strlen(input));
+
+    tsm_vte_unref(vte);
+    vte = NULL;
+
+    tsm_screen_unref(screen);
+    screen = NULL;
 }
 END_TEST

@@ -295,6 +301,12 @@
     tsm_vte_input(vte, input, strlen(input));
     ck_assert_ptr_eq(write_buffer, write_buffer_p);
     storing_write_cb_reset();
+
+    tsm_vte_unref(vte);
+    vte = NULL;
+
+    tsm_screen_unref(screen);
+    screen = NULL;
 }
 END_TEST

@@ -458,6 +470,97 @@
 }
 END_TEST

+static bool bell_cb_called = false;
+
+static void bell_cb(struct tsm_vte *vte, void *data)
+{
+    bool *flag = data;
+    ck_assert_ptr_ne(vte, NULL);
+    if (flag)
+        *flag = true;
+    bell_cb_called = true;
+}
+
+START_TEST(test_vte_bell)
+{
+    struct tsm_screen *screen;
+    struct tsm_vte *vte;
+    int r;
+
+    r = tsm_screen_new(&screen, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    r = tsm_vte_new(&vte, screen, write_cb, NULL, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    tsm_vte_set_bell_cb(vte, bell_cb, NULL);
+
+    bell_cb_called = false;
+    tsm_vte_input(vte, "\x07", 1);
+    ck_assert(bell_cb_called);
+
+    /* BEL inside an OSC sequence acts as the ST terminator, not as a bell */
+    bell_cb_called = false;
+    tsm_vte_input(vte, "\033]0;title\x07", 10);
+    ck_assert(!bell_cb_called);
+
+    tsm_vte_unref(vte);
+    vte = NULL;
+
+    tsm_screen_unref(screen);
+    screen = NULL;
+}
+END_TEST
+
+START_TEST(test_vte_bell_no_cb)
+{
+    struct tsm_screen *screen;
+    struct tsm_vte *vte;
+    int r;
+
+    r = tsm_screen_new(&screen, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    r = tsm_vte_new(&vte, screen, write_cb, NULL, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    /* BEL without a registered callback must not crash */
+    tsm_vte_input(vte, "\x07", 1);
+
+    tsm_vte_unref(vte);
+    vte = NULL;
+
+    tsm_screen_unref(screen);
+    screen = NULL;
+}
+END_TEST
+
+START_TEST(test_vte_bell_data)
+{
+    struct tsm_screen *screen;
+    struct tsm_vte *vte;
+    bool flag = false;
+    int r;
+
+    r = tsm_screen_new(&screen, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    r = tsm_vte_new(&vte, screen, write_cb, NULL, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    tsm_vte_set_bell_cb(vte, bell_cb, &flag);
+
+    tsm_vte_input(vte, "\x07", 1);
+    ck_assert(flag);
+
+    tsm_vte_unref(vte);
+    vte = NULL;
+
+    tsm_screen_unref(screen);
+    screen = NULL;
+}
+END_TEST
+
 TEST_DEFINE_CASE(misc)
     TEST(test_vte_init)
     TEST(test_vte_null)
@@ -470,10 +573,117 @@
     TEST(test_vte_csi_cursor_up_down)
 TEST_END_CASE

+TEST_DEFINE_CASE(bell)
+    TEST(test_vte_bell)
+    TEST(test_vte_bell_no_cb)
+    TEST(test_vte_bell_data)
+TEST_END_CASE
+
+static unsigned int led_cb_leds = 0;
+static bool led_cb_called = false;
+
+static void led_cb(struct tsm_vte *vte, unsigned int leds, void *data)
+{
+    unsigned int *out = data;
+    ck_assert_ptr_ne(vte, NULL);
+    if (out)
+        *out = leds;
+    led_cb_leds = leds;
+    led_cb_called = true;
+}
+
+START_TEST(test_vte_led_decll)
+{
+    struct tsm_screen *screen;
+    struct tsm_vte *vte;
+    int r;
+
+    r = tsm_screen_new(&screen, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    r = tsm_vte_new(&vte, screen, write_cb, NULL, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    tsm_vte_set_led_cb(vte, led_cb, NULL);
+
+    /* CSI 1 q  — turn on Scroll Lock LED */
+    led_cb_called = false;
+    tsm_vte_input(vte, "\033[1q", 4);
+    ck_assert(led_cb_called);
+    ck_assert_uint_eq(led_cb_leds, TSM_VTE_LED_SCROLL_LOCK);
+
+    /* CSI 3 q  — turn on Caps Lock LED (cumulative) */
+    led_cb_called = false;
+    tsm_vte_input(vte, "\033[3q", 4);
+    ck_assert(led_cb_called);
+    ck_assert_uint_eq(led_cb_leds, TSM_VTE_LED_SCROLL_LOCK | 
TSM_VTE_LED_CAPS_LOCK);
+
+    /* CSI 0 q  — clear all LEDs */
+    led_cb_called = false;
+    tsm_vte_input(vte, "\033[0q", 4);
+    ck_assert(led_cb_called);
+    ck_assert_uint_eq(led_cb_leds, 0);
+
+    tsm_vte_unref(vte);
+    tsm_screen_unref(screen);
+}
+END_TEST
+
+START_TEST(test_vte_led_no_cb)
+{
+    struct tsm_screen *screen;
+    struct tsm_vte *vte;
+    int r;
+
+    r = tsm_screen_new(&screen, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    r = tsm_vte_new(&vte, screen, write_cb, NULL, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    /* DECLL without a registered callback must not crash */
+    tsm_vte_input(vte, "\033[1q", 4);
+
+    tsm_vte_unref(vte);
+    tsm_screen_unref(screen);
+}
+END_TEST
+
+START_TEST(test_vte_led_data)
+{
+    struct tsm_screen *screen;
+    struct tsm_vte *vte;
+    unsigned int out = 0;
+    int r;
+
+    r = tsm_screen_new(&screen, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    r = tsm_vte_new(&vte, screen, write_cb, NULL, log_cb, NULL);
+    ck_assert_int_eq(r, 0);
+
+    tsm_vte_set_led_cb(vte, led_cb, &out);
+
+    tsm_vte_input(vte, "\033[2q", 4);
+    ck_assert_uint_eq(out, TSM_VTE_LED_NUM_LOCK);
+
+    tsm_vte_unref(vte);
+    tsm_screen_unref(screen);
+}
+END_TEST
+
+TEST_DEFINE_CASE(led)
+    TEST(test_vte_led_decll)
+    TEST(test_vte_led_no_cb)
+    TEST(test_vte_led_data)
+TEST_END_CASE
+
 // clang-format off
 TEST_DEFINE(
     TEST_SUITE(vte,
         TEST_CASE(misc),
+        TEST_CASE(bell),
+        TEST_CASE(led),
         TEST_END
     )
 )

Attachment: OpenPGP_0xC293E7B461825ACE.asc
Description: OpenPGP public key

Attachment: OpenPGP_signature.asc
Description: OpenPGP digital signature

Reply via email to