1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
#include "wsap_vtcore/wsap_vtcore.h"
#include "term_api.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static uint64_t state;
static unsigned random_below(unsigned limit)
{
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
return (unsigned)(state % limit);
}
/* Select from the oldest retained row to the end of the live screen.
* Selection text joins wrapped rows and trims blank cells on other rows. */
static char *logical_text(struct wsap_vtcore *t)
{
struct wsap_vtcore_vector2i size = wsap_vtcore_size(t);
wsap_vtcore_scroll_to(t, wsap_vtcore_scrollback(t).total);
wsap_vtcore_select_start(t, (struct wsap_vtcore_vector2i){ .x = 0, .y = 0 }, false, WSAP_VTCORE_SELECT_CELL);
wsap_vtcore_scroll_to(t, 0);
wsap_vtcore_select_extend(t, (struct wsap_vtcore_vector2i){ .x = size.x - 1, .y = size.y - 1 }, true);
char *text = tt_selection_text(t);
wsap_vtcore_select_clear(t);
if (!text) return NULL;
size_t out = 0;
for (size_t i = 0; text[i]; i++) {
if (text[i] == '\n') {
while (out && text[out - 1] == ' ') out--;
}
text[out++] = text[i];
}
while (out && (text[out - 1] == '\n' || text[out - 1] == ' ')) out--;
text[out] = 0;
return text;
}
static uint32_t cell_at(struct wsap_vtcore *t, struct wsap_vtcore_snapshot *s, int x, int y)
{
if (!wsap_vtcore_snapshot_update(s, t)) return 0;
return wsap_vtcore_snapshot_row(s, y)[x].text;
}
/* A height change that pulls rows back from history moves the saved
* cursor with its row. DECRC then prints next to the saved text. */
static int saved_cursor_follows_history(struct wsap_vtcore_snapshot *s)
{
static const struct { int cols, rows, grow_cols, grow_rows; } cases[] = {
{ 8, 3, 8, 5 }, /* same width: rows come back from history */
{ 8, 3, 10, 5 }, /* new width: reflow */
};
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
struct wsap_vtcore_options opt = {
.size = { .x = cases[i].cols, .y = cases[i].rows }, .scrollback = 10,
};
struct wsap_vtcore *t = wsap_vtcore_new(&opt);
if (!t) return 1;
const char *text = "l1\r\nl2\r\nl3\r\nl4\r\nab\0337\033[1;1H";
wsap_vtcore_feed(t, text, strlen(text), 0);
struct wsap_vtcore_vector2i size = { .x = cases[i].grow_cols, .y = cases[i].grow_rows };
if (!wsap_vtcore_resize(t, size)) return 1;
wsap_vtcore_feed(t, "\0338X", 3, 0);
struct wsap_vtcore_cursor c = wsap_vtcore_cursor(t);
int y = c.pos.y;
if (cell_at(t, s, 0, y) != 'a' || cell_at(t, s, 1, y) != 'b' ||
cell_at(t, s, 2, y) != 'X') {
fprintf(stderr, "case %zu: DECRC after a resize lost the saved row\n", i);
return 1;
}
wsap_vtcore_free(t);
}
return 0;
}
/* Narrowing a full screen with the cursor at the top wraps more rows than
* the ring holds. The cursor row must survive and stay on screen. */
static int narrow_resize_keeps_cursor_row(struct wsap_vtcore_snapshot *s)
{
struct wsap_vtcore_options opt = { .size = { .x = 40, .y = 12 }, .scrollback = 5 };
struct wsap_vtcore *t = wsap_vtcore_new(&opt);
if (!t) return 1;
for (int y = 0; y < 12; y++) {
char line[64];
memset(line, 'a' + y, 40);
line[40] = 0;
wsap_vtcore_feed(t, line, 40, 0);
}
wsap_vtcore_feed(t, "\033[1;1H", 6, 0);
if (!wsap_vtcore_resize(t, (struct wsap_vtcore_vector2i){ .x = 1, .y = 12 })) return 1;
struct wsap_vtcore_cursor c = wsap_vtcore_cursor(t);
if (c.pos.x != 0 || c.pos.y != 0 || cell_at(t, s, 0, 0) != 'a') {
fprintf(stderr, "narrow resize: cursor %d,%d, cell %x\n", c.pos.x, c.pos.y,
cell_at(t, s, 0, 0));
return 1;
}
wsap_vtcore_feed(t, "Z", 1, 0);
if (cell_at(t, s, 0, 0) != 'Z') return 1;
wsap_vtcore_free(t);
return 0;
}
int main(int argc, char **argv)
{
struct wsap_vtcore_snapshot *snap = wsap_vtcore_snapshot_new();
if (!snap || saved_cursor_follows_history(snap) || narrow_resize_keeps_cursor_row(snap))
return 1;
wsap_vtcore_snapshot_free(snap);
int seeds = argc > 1 ? atoi(argv[1]) : 100;
static const char *word[] = {
"alpha", " x", "中", "日", "e\xcc\x81", "🦊", "👍", " beta", "123", "a",
};
for (int seed = 1; seed <= seeds; seed++) {
state = 0x9e3779b97f4a7c15ull * (uint64_t)seed;
struct wsap_vtcore_options opt = {
.size = { .x = 40, .y = 12 },
.scrollback = 4096,
};
struct wsap_vtcore *t = wsap_vtcore_new(&opt);
if (!t) return 1;
for (int line = 0; line < 16; line++) {
int words = 1 + (int)random_below(4);
for (int i = 0; i < words; i++) {
const char *s = word[random_below(sizeof(word) / sizeof(word[0]))];
wsap_vtcore_feed(t, s, strlen(s), 0);
}
if (line != 15) wsap_vtcore_feed(t, "\r\n", 2, 0);
}
char *before = logical_text(t);
if (!before) return 1;
for (int step = 0; step < 20; step++) {
struct wsap_vtcore_vector2i size = {
.x = 1 + (int)random_below(200),
.y = 1 + (int)random_below(60),
};
if (!wsap_vtcore_resize(t, size)) return 1;
char *after = logical_text(t);
if (!after) return 1;
if (strcmp(before, after)) {
fprintf(stderr, "seed %d step %d resize %dx%d: logical text changed\n"
"before: <%s>\nafter: <%s>\n",
seed, step, size.x, size.y, before, after);
return 1;
}
free(after);
}
free(before);
wsap_vtcore_free(t);
}
return 0;
}
|