aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-26 10:57:03 +0100
committereunhhu <[email protected]>2026-09-26 10:57:03 +0100
commitf6be15d01ac184f070e1bb34af12689f68eb9140 (patch)
tree4ebfac5e6baf066b58136c2f71a70ab007ea5f5d
parent04e883a310fd3914d6f4294a92b08b3fe9f55686 (diff)
downloadvega-f6be15d01ac184f070e1bb34af12689f68eb9140.tar.gz
vega-f6be15d01ac184f070e1bb34af12689f68eb9140.zip
Move the focus from a surface: clicks, the wheel, point
Only the app's key handlers could move a list's focus. When the focus view is a plain index position (`view tasks_list_focus = at.position`), Vega now asks Pure for that index and moves it as a user pointing at a row (pure_app_point), with no code in the app. The grid remembers which item each line shows; in the TUI a click on a row focuses it and the wheel moves the focus one row (SGR mouse reports, turned off on exit). Scripts say point VIEW ROW or click LINE COLUMN on the last shown frame. The focus is stored like any event, so the next run of any surface starts from it. The model now points too: random scripts with points (some past the end) and TUI clicks still match it on every surface, a mouse click on a real pseudo-terminal moves the focus, and the stored session keeps it.
-rw-r--r--docs/design/001-first-component.md10
-rw-r--r--src/draw.c27
-rw-r--r--src/main.c70
-rw-r--r--src/screen.c8
-rw-r--r--src/vega.h7
-rw-r--r--tests/check.py51
6 files changed, 148 insertions, 25 deletions
diff --git a/docs/design/001-first-component.md b/docs/design/001-first-component.md
index 0161af9..fcfa3df 100644
--- a/docs/design/001-first-component.md
+++ b/docs/design/001-first-component.md
@@ -39,6 +39,12 @@ view tasks_list_focus = at.position
| CLI | `vega cli app.pmod < script` | 파이프용 평문. 목록은 `> ` 표시와 `[badge]`, 표는 표시 폭으로 정렬한 열. 터미널이고 `NO_COLOR`가 없을 때만 색 |
| NUI | `vega say app.pmod < script` | 문장. 이벤트마다 포커스 항목을 말하고(`Item 2 of 3: danger, milk, urgent.`), 실패하면 `Could not toggle.`, `show`에서 화면 전체 |
+포커스는 표면에서도 옮긴다. TUI에서 목록·표의 행을 클릭하면 그 행이 포커스가 되고, 휠은 포커스를 한 행씩 옮긴다
+(SGR 마우스 보고). 스크립트는 `point 보기 행`(0부터)과 `click 줄 칸`(마지막으로 보인 프레임, 1부터)으로 같은 일을 한다.
+앱에 코드가 필요 없다. `<표>_focus` 보기가 `at.position`처럼 인덱스 상태의 위치 그대로이면 Vega는 Pure에게 그 상태를
+묻고(`pure_module_position`) 행을 가리키는 이벤트로 옮긴다(`pure_app_point`). 키 이벤트처럼 저장되므로 다음 실행의 다른
+표면도 그 포커스에서 이어 간다. 포커스 보기가 다른 식이면(계산한 위치 등) 가리킬 상태가 없어 클릭은 아무 일도 하지 않는다.
+
TUI 입력: 앱에 text 입력이 있으면 처음에는 글자가 입력 줄로 가고 Enter가 확정한다(IME 확정분과 같은 의미).
Tab이 키 모드로 바꾸면 글자·숫자·Space가 `KeyA`·`Digit0`·`Space` 입력이 된다(앱이 Tab을 쓰면 Tab은 앱의 것).
화살표·Delete·Home 같은 키는 두 모드 모두 앱으로 간다. Ctrl-C나 Ctrl-Q로 끝내고 터미널을 되돌린다.
@@ -100,10 +106,10 @@ CLI나 문장 표면이 그대로 이어받는다. Vega에도 앱에도 저장 �
- aup와 Vega의 예제 11개를 세 표면에 무작위 입력으로 돌려 비정상 종료가 없다. sanitizer 빌드로도 같은 검사를 통과한다.
- 규약을 여덟 가지로 어긴 명세에서 `vega check`가 아홉 곳을 모두 정확한 문장으로 알리고, 예제에서는 아무것도 알리지
않는다.
+- 무작위 스크립트에 `point`(범위 밖 포함)와 TUI `click`을 섞어도 CLI·문장·프레임이 모델과 같다. 가상 터미널에서
+ 마우스로 행을 클릭하면 포커스가 옮겨지고, 저장소를 쓰는 다음 실행이 그 포커스에서 시작한다.
## 남은 것
-- 포커스를 표면에서 바꾸기(GUI 클릭, TUI 마우스). 지금은 앱의 키 핸들러만 포커스를 옮긴다. Pure에 표의 인덱스를
- 이벤트로 바꾸는 호스트 API가 필요하다.
- GUI 표면, 음성 합성으로 가는 NUI.
- 문장의 언어. 지금 NUI의 틀 문장은 영어이고 값은 앱의 text 그대로다.
diff --git a/src/draw.c b/src/draw.c
index bec3804..3c7a59d 100644
--- a/src/draw.c
+++ b/src/draw.c
@@ -193,11 +193,15 @@ void vega_say(FILE *out, PureApp *app, const VegaScreen *screen, bool all) {
bool vega_grid(VegaGrid *grid, unsigned width, unsigned height) {
size_t count = (size_t)width * height;
VegaCell *cells = calloc(count ? count : 1, sizeof(*cells)), *shown = calloc(count ? count : 1, sizeof(*shown));
- if (!cells || !shown) { free(cells); free(shown); return false; }
+ struct VegaRow *rows = calloc(height ? height : 1, sizeof(*rows));
+ if (!cells || !shown || !rows) { free(cells); free(shown); free(rows); return false; }
+ for (unsigned r = 0; r < height; ++r) rows[r].part = -1;
free(grid->cells);
free(grid->shown);
+ free(grid->rows);
grid->cells = cells;
grid->shown = shown;
+ grid->rows = rows;
grid->painted = false;
grid->width = width;
grid->height = height;
@@ -207,6 +211,7 @@ bool vega_grid(VegaGrid *grid, unsigned width, unsigned height) {
void vega_grid_free(VegaGrid *grid) {
free(grid->cells);
free(grid->shown);
+ free(grid->rows);
*grid = (VegaGrid){0};
}
@@ -272,11 +277,13 @@ static const char *key_symbol(const char *key) {
return key;
}
-static void draw_list(VegaGrid *grid, PureApp *app, const VegaPart *part, size_t first, unsigned top, unsigned height) {
+static void draw_list(VegaGrid *grid, PureApp *app, const VegaPart *part, int index, size_t first, unsigned top,
+ unsigned height) {
char text[VEGA_TEXT];
size_t length, rows = vega_rows(app, part), focus = vega_focus(app, part);
for (unsigned r = 0; r < height && first + r < rows; ++r) {
size_t row = first + r;
+ grid->rows[top + r] = (struct VegaRow){index, row};
bool focused = row == focus;
VegaTone tone = vega_tone(app, part, row);
Pen pen = {grid, top + r, 0, grid->width, (uint8_t)tone, focused, false};
@@ -307,7 +314,8 @@ static void draw_list(VegaGrid *grid, PureApp *app, const VegaPart *part, size_t
}
}
-static void draw_table(VegaGrid *grid, PureApp *app, const VegaPart *part, size_t first, unsigned top, unsigned height) {
+static void draw_table(VegaGrid *grid, PureApp *app, const VegaPart *part, int index, size_t first, unsigned top,
+ unsigned height) {
const PureModule *m = app->module;
char text[VEGA_TEXT];
size_t length, rows = vega_rows(app, part), focus = vega_focus(app, part);
@@ -338,6 +346,7 @@ static void draw_table(VegaGrid *grid, PureApp *app, const VegaPart *part, size_
for (unsigned r = 0; r < visible && first + r < rows; ++r) {
size_t row = first + r;
bool focused = row == focus;
+ grid->rows[top + 1 + r] = (struct VegaRow){index, row};
Pen pen = {grid, top + 1 + r, 0, grid->width, (uint8_t)vega_tone(app, part, row), focused, false};
write_cstr(&pen, focused ? "> " : " ");
for (unsigned c = 0; c < part->column_count; ++c) {
@@ -351,10 +360,18 @@ static void draw_table(VegaGrid *grid, PureApp *app, const VegaPart *part, size_
}
}
+bool vega_hit(const VegaGrid *grid, unsigned line, unsigned *part, size_t *item) {
+ if (line >= grid->height || grid->rows[line].part < 0) return false;
+ *part = (unsigned)grid->rows[line].part;
+ *item = grid->rows[line].item;
+ return true;
+}
+
void vega_draw(VegaGrid *grid, PureApp *app, const VegaScreen *screen, const char *prompt, size_t prompt_length,
bool typing) {
const PureModule *m = app->module;
for (size_t i = 0; i < (size_t)grid->width * grid->height; ++i) grid->cells[i] = (VegaCell){' ', 0, 0, false, false};
+ for (unsigned r = 0; r < grid->height; ++r) grid->rows[r].part = -1;
grid->cursor_row = grid->cursor_column = -1;
if (!grid->width || !grid->height) return;
char text[VEGA_TEXT];
@@ -398,8 +415,8 @@ void vega_draw(VegaGrid *grid, PureApp *app, const VegaScreen *screen, const cha
if (focus < *first) *first = focus;
if (visible && focus >= *first + visible) *first = focus - visible + 1;
if (*first + visible > rows) *first = rows > visible ? rows - visible : 0;
- if (part->kind == VEGA_LIST) draw_list(grid, app, part, *first, row + 1, area);
- else draw_table(grid, app, part, *first, row + 1, area);
+ if (part->kind == VEGA_LIST) draw_list(grid, app, part, (int)i, *first, row + 1, area);
+ else draw_table(grid, app, part, (int)i, *first, row + 1, area);
row += share;
}
/* Keys the app answers to, then the text being typed. */
diff --git a/src/main.c b/src/main.c
index 4f83780..4e5c79b 100644
--- a/src/main.c
+++ b/src/main.c
@@ -20,13 +20,16 @@
* With --store the app's states live in that tree store: the app continues
* where it was, whichever surface runs it.
* Script commands, one per line: key KEYNAME | text HEX|- | type HEX | enter |
- * tab | frame [N] | show. key presses and releases the input bound to that key;
- * text commits text to the screen's text input; type, enter and tab act on the
- * TUI's input line. */
+ * tab | frame [N] | point VIEW ROW | click LINE COLUMN | show. key presses and
+ * releases the input bound to that key; text commits text to the screen's text
+ * input; type, enter and tab act on the TUI's input line; point focuses a row
+ * (0 first) of a list or table; click is a mouse click on the last shown TUI
+ * frame (1 first, as terminals report it). */
static PureModule module;
static PureApp app;
static VegaScreen screen;
+static VegaGrid *drawn; /* the TUI's last frame, which a click lands on */
static char entry[VEGA_TEXT];
static size_t entry_length;
static bool typing;
@@ -85,6 +88,27 @@ static bool commit(const char *text, size_t length) {
return pure_app_text(&app, (uint32_t)screen.prompt, (PureSpan){(const unsigned char *)text, length}) == PURE_OK;
}
+static const VegaPart *part_named(const char *name) {
+ for (unsigned i = 0; i < screen.count; ++i)
+ if (screen.parts[i].kind != VEGA_FIELD && !strcmp(module.view_names[screen.parts[i].view], name)) return &screen.parts[i];
+ return NULL;
+}
+
+/* A mouse button on a line of the last frame: a click focuses the row there;
+ * the wheel moves the focus of the list or table under it. */
+static bool mouse(unsigned button, unsigned line) {
+ unsigned index;
+ size_t item;
+ if (!drawn || !vega_hit(drawn, line, &index, &item)) return false;
+ const VegaPart *part = &screen.parts[index];
+ if (button == 64 || button == 65) {
+ size_t focus = vega_focus(&app, part), rows = vega_rows(&app, part);
+ if (button == 64 ? !focus : focus + 1 >= rows) return false;
+ item = button == 64 ? focus - 1 : focus + 1;
+ } else if (button) return false;
+ return vega_point(&app, part, item);
+}
+
static int hex(char c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; }
static size_t unhex(const char *digits, char *out, size_t size) {
@@ -221,7 +245,7 @@ static void on_resize(int signal_number) { (void)signal_number; resized = 1; }
static void restore_terminal(void) {
if (!raw_terminal) return;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_terminal);
- const char *leave = "\033[0m\033[?25h\033[?1049l";
+ const char *leave = "\033[?1006l\033[?1000l\033[0m\033[?25h\033[?1049l";
if (write(STDOUT_FILENO, leave, strlen(leave)) < 0) { /* nothing left to tell */ }
raw_terminal = false;
}
@@ -238,7 +262,7 @@ static bool raw_mode(void) {
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &t)) return false;
raw_terminal = true;
atexit(restore_terminal);
- const char *enter = "\033[?1049h\033[?25l";
+ const char *enter = "\033[?1049h\033[?25l\033[?1000h\033[?1006h"; /* mouse buttons, reported as SGR */
return write(STDOUT_FILENO, enter, strlen(enter)) >= 0;
}
@@ -286,6 +310,19 @@ static bool handle_input(const unsigned char *in, size_t length) {
for (size_t at = 0; at < length;) {
unsigned char c = in[at];
if (c == 0x03 || c == 0x11) return false; /* Ctrl-C, Ctrl-Q */
+ if (c == 0x1B && at + 2 < length && in[at + 1] == '[' && in[at + 2] == '<') {
+ /* A mouse report: ESC [ < button ; column ; line, M on press, m on release. */
+ unsigned fields[3] = {0, 0, 0}, field = 0;
+ size_t end = at + 3;
+ for (; end < length && field < 3; ++end) {
+ if (in[end] >= '0' && in[end] <= '9') fields[field] = fields[field] * 10 + (unsigned)(in[end] - '0');
+ else if (in[end] == ';') ++field;
+ else break;
+ }
+ if (end < length && in[end] == 'M' && field == 2 && fields[2]) mouse(fields[0], fields[2] - 1);
+ at = end + 1;
+ continue;
+ }
if (c == 0x1B) {
size_t matched = 0;
for (size_t i = 0; i < sizeof(sequences) / sizeof(*sequences) && !matched; ++i) {
@@ -318,6 +355,7 @@ static int interactive(void) {
if (!raw_mode()) { fputs("vega: the tui needs a terminal (or use --dump WxH)\n", stderr); return 2; }
signal(SIGWINCH, on_resize);
VegaGrid grid = {0};
+ drawn = &grid;
bool ticking = frames();
for (;;) {
if (resized) {
@@ -338,6 +376,7 @@ static int interactive(void) {
if (got <= 0 || !handle_input(in, (size_t)got)) break;
}
restore_terminal();
+ drawn = NULL;
vega_grid_free(&grid);
return 0;
}
@@ -351,6 +390,8 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
static char line[1 << 16];
VegaGrid grid = {0};
if (surface == SURFACE_TUI && !vega_grid(&grid, width, height)) return 2;
+ bool shown = false;
+ drawn = surface == SURFACE_TUI ? &grid : NULL;
bool color = surface == SURFACE_CLI && isatty(STDOUT_FILENO) && !getenv("NO_COLOR");
unsigned long counted = vega_evaluations();
while (fgets(line, sizeof(line), stdin)) {
@@ -359,6 +400,7 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
if (sscanf(line, "%15s %65535s", command, argument) < 1) continue;
bool acted = true, ok = true;
int input = -1;
+ const char *verb = "do that";
if (!strcmp(command, "key")) {
ok = press(argument, &input);
if (input < 0) fprintf(stderr, "vega: no input answers %s\n", argument);
@@ -373,6 +415,19 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
} else if (!strcmp(command, "tab")) {
handle_key("Tab");
acted = false;
+ } else if (!strcmp(command, "point")) {
+ char name[PURE_MODULE_NAME];
+ long row = -1;
+ const VegaPart *part = sscanf(line, "%*s %31s %ld", name, &row) == 2 ? part_named(name) : NULL;
+ verb = "point";
+ if (!part) fprintf(stderr, "vega: no list or table %s\n", name);
+ ok = part && row >= 0 && vega_point(&app, part, (size_t)row);
+ } else if (!strcmp(command, "click")) {
+ unsigned at_line = 0, at_column = 0;
+ verb = "point";
+ if (surface != SURFACE_TUI) { fputs("vega: click needs the tui\n", stderr); continue; }
+ acted = sscanf(line, "%*s %u %u", &at_line, &at_column) == 2 && at_line && shown;
+ ok = acted && mouse(0, at_line - 1);
} else if (!strcmp(command, "frame")) {
long count = *argument ? strtol(argument, NULL, 10) : 1;
for (long i = 0; i < count && i < 600; ++i) ok = pure_app_frame(&app) == PURE_OK && ok;
@@ -380,7 +435,7 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
acted = false;
if (surface == SURFACE_CLI) vega_print(stdout, &app, &screen, color);
else if (surface == SURFACE_SAY) vega_say(stdout, &app, &screen, true);
- else if (painting) {
+ else if ((shown = true) && painting) {
Out out = {0};
vega_draw(&grid, &app, &screen, entry, entry_length, typing);
paint(&grid, &out);
@@ -400,11 +455,12 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
}
if (!ok && input >= 0) fprintf(stderr, "vega: %s failed\n", module.input_names[input]);
if (surface == SURFACE_SAY && acted) {
- if (!ok) printf("Could not %s.\n", input >= 0 ? module.input_names[input] : "do that");
+ if (!ok) printf("Could not %s.\n", input >= 0 ? module.input_names[input] : verb);
else vega_say(stdout, &app, &screen, false);
}
fflush(stdout);
}
+ drawn = NULL;
vega_grid_free(&grid);
return 0;
}
diff --git a/src/screen.c b/src/screen.c
index 2d4c6dc..3e8b2c4 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -32,12 +32,14 @@ void vega_screen(VegaScreen *screen, const PureModule *m) {
for (uint32_t i = 0; i < m->views && screen->count < VEGA_PARTS; ++i) {
if (m->view_table[i] || (int)i == screen->title || focus_view[i]) continue;
VegaPart *part = &screen->parts[screen->count++];
- *part = (VegaPart){.kind = VEGA_FIELD, .view = i, .focus = -1, .label = -1, .tone = -1, .detail = -1, .badge = -1};
+ *part = (VegaPart){.kind = VEGA_FIELD, .view = i, .focus = -1, .pointer = -1, .label = -1, .tone = -1, .detail = -1,
+ .badge = -1};
if (m->view_type[i] != PURE_VIEW_TABLE) continue;
char name[PURE_MODULE_NAME + 8];
snprintf(name, sizeof(name), "%s_focus", m->view_names[i]);
part->focus = pure_module_view(m, name);
if (part->focus >= 0 && !focus_view[part->focus]) part->focus = -1;
+ part->pointer = part->focus >= 0 ? pure_module_position(m, (uint32_t)part->focus) : -1;
part->label = column(m, i, "label", PURE_VIEW_TEXT);
part->tone = column(m, i, "tone", PURE_VIEW_NUM);
part->kind = part->label >= 0 ? VEGA_LIST : VEGA_TABLE;
@@ -155,6 +157,10 @@ size_t vega_focus(PureApp *app, const VegaPart *part) {
return focus >= (double)(rows - 1) ? rows - 1 : (size_t)focus;
}
+bool vega_point(PureApp *app, const VegaPart *part, size_t row) {
+ return part->pointer >= 0 && pure_app_point(app, (uint32_t)part->pointer, row) == PURE_OK;
+}
+
VegaTone vega_tone(PureApp *app, const VegaPart *part, size_t row) {
double tone;
if (part->tone < 0 || !vega_number(app, (uint32_t)part->tone, row, &tone)) return VEGA_NORMAL;
diff --git a/src/vega.h b/src/vega.h
index 2d0a29f..80e3f7d 100644
--- a/src/vega.h
+++ b/src/vega.h
@@ -20,6 +20,7 @@ typedef struct {
VegaKind kind;
uint32_t view;
int focus; /* number view with the focused row, or -1 */
+ int pointer; /* the index state that view shows, or -1: a surface may move it */
int label, tone, detail, badge; /* columns, or -1 */
uint32_t columns[PURE_MODULE_VIEWS]; /* a table's shown columns, in order */
unsigned column_count;
@@ -47,6 +48,9 @@ bool vega_number(PureApp *, uint32_t view, size_t row, double *out);
size_t vega_rows(const PureApp *, const VegaPart *);
/* The focused row, clamped to the rows; 0 without a focus view. */
size_t vega_focus(PureApp *, const VegaPart *);
+/* Focus a row, as a user pointing at it (a click); false when the part's
+ * focus is not a plain index position or the row is not there. */
+bool vega_point(PureApp *, const VegaPart *, size_t row);
VegaTone vega_tone(PureApp *, const VegaPart *, size_t row);
/* Values evaluated so far, for checking that surfaces stay lazy. */
unsigned long vega_evaluations(void);
@@ -77,6 +81,7 @@ typedef struct {
typedef struct {
unsigned width, height;
VegaCell *cells, *shown;
+ struct VegaRow { int part; size_t item; } *rows; /* the item each line shows, or part -1 */
bool painted; /* shown holds what the terminal shows */
size_t first[VEGA_PARTS];
int cursor_row, cursor_column; /* where the terminal cursor goes, or -1 */
@@ -86,4 +91,6 @@ void vega_grid_free(VegaGrid *);
/* prompt is the text being typed for the screen's text input; typing says
* whether keys go to it. */
void vega_draw(VegaGrid *, PureApp *, const VegaScreen *, const char *prompt, size_t prompt_length, bool typing);
+/* The part and item a line of the last drawn grid shows. */
+bool vega_hit(const VegaGrid *, unsigned line, unsigned *part, size_t *item);
#endif
diff --git a/tests/check.py b/tests/check.py
index d46698d..e0d5996 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -61,7 +61,13 @@ class Todo:
self.tasks, self.at = [], 0
def event(self, name, text=None):
- """Returns False when the event fails (a missing item)."""
+ """Returns False when the event fails (a missing item). point takes
+ the row as text: a surface focusing it, as a click would."""
+ if name == 'point':
+ if not 0 <= text < len(self.tasks):
+ return False
+ self.at = text
+ return True
if name == 'add':
self.tasks.append({'title': text, 'done': 0, 'urgent': 0})
self.at = len(self.tasks) - 1
@@ -127,7 +133,12 @@ def cli_and_say(vega, module, seeds, steps=300):
rng = random.Random(seed)
model, script, expected_cli, expected_say = Todo(), [], [], []
for _ in range(steps):
- if rng.random() < 0.25:
+ roll = rng.random()
+ if roll < 0.1:
+ row = rng.randrange(len(model.tasks) + 2)
+ script.append(f'point tasks_list {row}')
+ ok, name = model.event('point', row), 'point'
+ elif roll < 0.35:
word = rng.choice(WORDS)
script.append('text ' + (word.encode().hex() or '-'))
ok, name = model.event('add', word), 'add'
@@ -164,7 +175,12 @@ def stored_surfaces(vega, module, work, seeds, steps=240):
surface = ['cli', 'say'][number % 2]
script, expected = [], []
for _ in range(start, end):
- if rng.random() < 0.3:
+ roll = rng.random()
+ if roll < 0.1:
+ row = rng.randrange(len(model.tasks) + 1)
+ script.append(f'point tasks_list {row}')
+ ok, name = model.event('point', row), 'point'
+ elif roll < 0.4:
word = rng.choice(WORDS)
script.append('text ' + (word.encode().hex() or '-'))
ok, name = model.event('add', word), 'add'
@@ -201,9 +217,16 @@ def tui(vega, module, seeds, sizes, steps=300):
area = rows - 5 # title, field, list header, key bar, input line
for seed in seeds:
rng = random.Random(seed * 7 + columns)
- model, script, states = Todo(), [], []
- for _ in range(steps):
- if rng.random() < 0.3:
+ model, script, states, scroll = Todo(), [], [], 0
+ for step in range(steps):
+ roll = rng.random()
+ if roll < 0.12 and step:
+ # A click on a line of the last frame: an item line focuses that item.
+ line = rng.randrange(rows)
+ script.append(f'click {line + 1} 3')
+ if 3 <= line < 3 + area and scroll + line - 3 < len(model.tasks):
+ model.event('point', scroll + line - 3)
+ elif roll < 0.4:
word = rng.choice(WORDS)
script.append('text ' + (word.encode().hex() or '-'))
model.event('add', word)
@@ -212,6 +235,10 @@ def tui(vega, module, seeds, sizes, steps=300):
script.append(f'key {KEYS[name]}')
model.event(name)
script.append('show')
+ n = len(model.tasks)
+ if model.at < scroll: scroll = model.at
+ if model.at >= scroll + area: scroll = model.at - area + 1
+ if scroll + area > n: scroll = max(n - area, 0)
states.append(([dict(t) for t in model.tasks], model.at))
result = run(vega, 'tui', module, '--dump', f'{columns}x{rows}', stdin='\n'.join(script) + '\n')
assert result.returncode == 0, result.stderr
@@ -382,7 +409,8 @@ def interactive(vega, module, store=None):
return data
screen = read_for(0.3)
- for keys in (b'milk\r', 'b\u00e9ton\r'.encode(), b'\t', b'\x1b[A', b' ', b'u'):
+ # Then a click on line 5 (béton's row): a mouse report, press and release.
+ for keys in (b'milk\r', 'b\u00e9ton\r'.encode(), b'\t', b'\x1b[A', b' ', b'u', b'\x1b[<0;3;5M\x1b[<0;3;5m'):
os.write(fd, keys)
screen += read_for(0.2)
os.write(fd, b'\x03')
@@ -395,11 +423,13 @@ def interactive(vega, module, store=None):
# What the terminal ended up showing, from the bytes it was sent.
grid = [[[' ', 0, False] for _ in range(40)] for _ in range(10)]
replay(screen, grid)
- final = '\n'.join(shown(grid))
- assert 'milk' in final and 'béton' in final and '(2)' in final and 'done' in final, final
+ final = shown(grid)
+ assert 'milk' in '\n'.join(final) and '(2)' in final[2] and 'done' in final[3], final
+ assert final[4].startswith('>> béton') and final[3].startswith('| milk'), final # the click moved the focus
+ assert '\x1b[?1000h' in text and '\x1b[?1000l' in text, 'mouse reports on, then off'
if store: # what was typed is still there in the next session
again = run(vega, 'cli', module, '--store', store, stdin='show\n').stdout.splitlines()
- assert again == ['To do', 'left: 2 tasks', 'tasks_list (2)', '> milk [done] (muted)', ' béton'], again
+ assert again == ['To do', 'left: 2 tasks', 'tasks_list (2)', ' milk [done] (muted)', '> béton'], again
return 1
@@ -479,6 +509,7 @@ def robustness(vega, aup, work, rounds):
elif roll < 0.3: script.append('show')
elif roll < 0.35: script.append('frame 3')
elif roll < 0.4: script.append(rng.choice(['type 61', 'enter', 'tab']))
+ elif roll < 0.45: script.append(f'click {rng.randrange(1, 30)} {rng.randrange(1, 90)}')
else: script.append('key ' + rng.choice(keys))
text = '\n'.join(script) + '\nshow\n'
size = rng.choice(['80x24', '12x5', '1x1', '200x3'])