summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/column_drag.zig89
-rw-r--r--test/column_grip.py132
-rw-r--r--test/snapshots/colgrip.golden170
-rw-r--r--test/snapshots/colgrip.snap35
4 files changed, 424 insertions, 2 deletions
diff --git a/test/column_drag.zig b/test/column_drag.zig
index 85dca66b..b2b8f35d 100644
--- a/test/column_drag.zig
+++ b/test/column_drag.zig
@@ -176,7 +176,7 @@ test "column grip is separate from compact text and the resize seam" {
p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = seam, .row = body_row } });
}
-test "column grips stay blank and muted across focus hover and drag" {
+test "column grips stay blank, and muted across focus and hover until grabbed" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 20 });
defer p.deinit();
_ = try p.setTestFile("left\n");
@@ -198,11 +198,12 @@ test "column grips stay blank and muted across focus hover and drag" {
_ = frame.reset(.retain_capacity);
const surface = try p.render(frame.allocator());
const muted: pardes.Color = .{ .rgb = p.chromeTheme().column_box_dim };
+ const lit: pardes.Color = .{ .rgb = p.chromeTheme().column_box };
for (0..p.ncol) |column| {
for (0..config.GUTTER) |offset| {
const cell = surface.at(p.col_x[column] + @as(u16, @intCast(offset)), pardes.TOPBAR_H);
try std.testing.expectEqualStrings(" ", cell.grapheme());
- try std.testing.expectEqual(muted, cell.style.bg);
+ try std.testing.expectEqual(if (dragging and column == active_column) lit else muted, cell.style.bg);
try std.testing.expectEqual(pardes.FontRole.tagline, cell.style.font_role);
}
try std.testing.expectEqual(p.col_x[column] + pardes.TAG_TEXT_INSET, (try tagLayer(p, column)).viewport.x);
@@ -252,3 +253,87 @@ test "column grip dropped in its own place moves the column's left edge" {
try std.testing.expectEqual(serials[1], p.col_serial[0]);
try std.testing.expectEqual(serials[0], p.col_serial[1]);
}
+
+/// The column of the drag preview's rail, checked to run the full height
+/// below the workspace tag in the grabbed grip's color, except through the
+/// grip and pane boxes at the window's left edge.
+fn previewRail(p: *Pardes, frame: *std.heap.ArenaAllocator) !?u16 {
+ _ = frame.reset(.retain_capacity);
+ const surface = try p.render(frame.allocator());
+ const lit: pardes.Color = .{ .rgb = p.chromeTheme().column_box };
+ var found: ?u16 = null;
+ for (0..surface.cols) |x| {
+ if (!std.mem.eql(u8, surface.at(@intCast(x), surface.rows - 1).grapheme(), "╎")) continue;
+ try std.testing.expect(found == null);
+ found = @intCast(x);
+ for (p.topBarHeight()..surface.rows) |row| {
+ const cell = surface.at(@intCast(x), @intCast(row));
+ if (x < config.GUTTER and cell.style.font_role == .tagline) {
+ try std.testing.expect(!std.mem.eql(u8, cell.grapheme(), "╎"));
+ continue;
+ }
+ try std.testing.expectEqualStrings("╎", cell.grapheme());
+ try std.testing.expectEqual(lit, cell.style.fg);
+ }
+ }
+ return found;
+}
+
+test "column grip drag previews where the column lands, in the seam beside its rule" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 24 });
+ defer p.deinit();
+ _ = try p.setTestFile("body\n");
+ p.presentation.enabled = false;
+ p.settings.column_tags = true;
+ for (0..2) |_| {
+ try std.testing.expect(p.executeBuiltinLine(p.active, "New"));
+ try std.testing.expect(layout.splitColumn(p, p.active, p.active, false));
+ }
+ try std.testing.expectEqual(@as(usize, 3), p.ncol);
+ p.update(.tick);
+ var frame = std.heap.ArenaAllocator.init(p.gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const serials = .{ p.col_serial[0], p.col_serial[1], p.col_serial[2] };
+
+ // Held on its own grip it lands nowhere new: no rail.
+ mouse(p, .press, p.col_x[1]);
+ mouse(p, .drag, p.col_x[1] + 1);
+ try std.testing.expectEqual(@as(?u16, null), try previewRail(p, &frame));
+ // Over the first column's left half it goes first. The window edge has
+ // no seam, and the gutter there is the GUI's scroll rail, so the rail
+ // runs in the gutter's last cell.
+ mouse(p, .drag, 3);
+ try std.testing.expectEqual(@as(?u16, config.GUTTER - 1), try previewRail(p, &frame));
+ // The pointer is over the first column's "New", but a carried column
+ // aims at a place: no word lights under it.
+ for (0..p.screen_w) |x| try std.testing.expect(!std.meta.eql(p.surface.at(@intCast(x), pardes.TOPBAR_H).style.bg, pardes.Color{ .rgb = p.theme().sel_bg }));
+ mouse(p, .release, 3);
+ try std.testing.expectEqual(serials[1], p.col_serial[0]);
+ try std.testing.expectEqual(serials[0], p.col_serial[1]);
+
+ // Past the middle column's middle it lands after it: the rail is in the
+ // seam cell left of the last column's rule, where a border drag's is.
+ mouse(p, .press, p.col_x[0]);
+ const past = p.col_x[1] + p.col_w[1] - 2;
+ mouse(p, .drag, past);
+ try std.testing.expectEqual(@as(?u16, p.col_x[2] - 1), try previewRail(p, &frame));
+ // Past the last column's middle: the window's last cell.
+ mouse(p, .drag, p.col_x[2] + p.col_w[2] - 1);
+ try std.testing.expectEqual(@as(?u16, p.screen_w - 1), try previewRail(p, &frame));
+ mouse(p, .release, p.col_x[2] + p.col_w[2] - 1);
+ try std.testing.expectEqual(serials[0], p.col_serial[0]);
+ try std.testing.expectEqual(serials[2], p.col_serial[1]);
+ try std.testing.expectEqual(serials[1], p.col_serial[2]);
+
+ // Short of a neighbour's middle the left edge moves, and the preview
+ // is the seam that edge will have: exactly where the release puts it.
+ const edge = p.col_x[2];
+ mouse(p, .press, edge + 1);
+ mouse(p, .drag, edge + 1 - 6);
+ try std.testing.expectEqual(@as(?u16, edge - 6 - 1), try previewRail(p, &frame));
+ mouse(p, .release, edge + 1 - 6);
+ _ = try p.render(frame.allocator());
+ try std.testing.expectEqual(edge - 6, p.col_x[2]);
+ try std.testing.expectEqual(@as(?u16, null), try previewRail(p, &frame));
+}
diff --git a/test/column_grip.py b/test/column_grip.py
new file mode 100644
index 00000000..ac0341c3
--- /dev/null
+++ b/test/column_grip.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+"""Drag a column grip in the real SDL window with pixel mouse events.
+
+WAYLAND_DISPLAY=/run/user/1000/wayland-1 \\
+ python3 -B test/column_grip.py zig-out/bin/pardes-gui /tmp/pardes-column-grip [--pad 7]
+
+The window is hidden (PARDES_TEST). Mouse events enter as ESC]777;mouse
+sequences on stdin, which the GUI builds into SDL events and dispatches like
+its own, so pixel-to-cell and tag hit testing run as they do under a hand.
+9P observes the grid; each step leaves a PNG of the GPU frame in the output
+directory: grip-rest, grip-resize-drag, grip-reorder-drag, grip-done.
+"""
+import argparse
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import time
+
+from fs import session
+from ui_review import require, wait_for
+
+
+def rows(client):
+ frame = client.screen()
+ cols = frame['cols']
+ return [''.join(cell[0] for cell in frame['cells'][at:at + cols]) for at in range(0, len(frame['cells']), cols)]
+
+
+def rail(client):
+ """The one column the drag preview's rail runs down, if any."""
+ bottom = rows(client)[-1]
+ found = [x for x, ch in enumerate(bottom) if ch == '╎']
+ require(len(found) <= 1, f'more than one rail: {found}')
+ return found[0] if found else None
+
+
+def grab(root, label):
+ time.sleep(.5)
+ def convert():
+ shutil.copyfile(root / 'latest.ppm', root / (label + '.ppm'))
+ return subprocess.run(['magick', str(root / (label + '.ppm')), str(root / (label + '.png'))],
+ stderr=subprocess.DEVNULL).returncode == 0
+ require(wait_for(convert, 8), 'GPU capture did not contain a complete image')
+ return root / (label + '.png')
+
+
+def pixel(path, x, y):
+ out = subprocess.run(['magick', str(path), '-format', f'%[pixel:p{{{x},{y}}}]', 'info:'],
+ capture_output=True, text=True, check=True)
+ return out.stdout.strip()
+
+
+def run(args):
+ root = args.output.resolve()
+ shutil.rmtree(root, ignore_errors=True)
+ root.mkdir(parents=True, mode=0o700)
+ (root / 'config/pardes').mkdir(parents=True)
+ (root / 'config/pardes/init').write_text('Shell /bin/sh\n')
+ (root / 'w.txt').write_text('alpha\nbeta\n')
+ name = 'column-grip-' + str(os.getpid())
+ launch = ['-c', 'stty cols 100 rows 30; exec "$@"', 'column-grip',
+ str(args.binary.resolve()), '--9p=' + name, str(root / 'w.txt')]
+ env = {'PARDES_TEST': '1', 'PARDES_TEST_CAPTURE_DIR': str(root), 'PARDES_TEST_COLS': '100',
+ 'PARDES_TEST_ROWS': '30', 'PARDES_TEST_PAD': str(args.pad)}
+ handles = {}
+ with session('/bin/sh', root, name, socket_name=name, tty=True, launch=launch,
+ inherited=env, terminal=handles) as (client, _):
+ def send(data):
+ os.write(handles['input_fd'], data.encode())
+
+ def mouse(kind, button, col, row):
+ # the middle of a grid cell, in window pixels
+ send(f'\x1b]777;mouse;{kind};{button};{(col + .5) * cw};{(row + .5) * ch}\x07')
+ time.sleep(.15)
+
+ require(wait_for(lambda: 'Newcol' in rows(client)[0]), 'workspace tag never drew')
+ send('\x1bn')
+ time.sleep(.3)
+ send('\x1bc')
+ require(wait_for(lambda: rows(client)[1].count('New Tty') == 2), 'no second column')
+ require(wait_for(lambda: (root / 'latest.ppm').exists(), 8), 'GPU capture missing')
+ time.sleep(.5)
+ with open(root / 'latest.ppm', 'rb') as ppm:
+ ppm.readline()
+ width, height = map(int, ppm.readline().split())
+ frame = client.screen()
+ cw = (width - args.pad) // frame['cols']
+ ch = (height - args.pad) // frame['rows']
+ edge = rows(client)[1].index('New Tty', 10) - 3
+
+ rest = grab(root, 'grip-rest')
+ mouse('down', 1, edge, 1)
+ held = grab(root, 'grip-held')
+ require(pixel(held, int((edge + 1) * cw), int(1.5 * ch)) != pixel(rest, int((edge + 1) * cw), int(1.5 * ch)),
+ 'a grabbed grip looks the same as a resting one')
+ require(rail(client) is None, 'a grip held in place previews a move')
+
+ # Short of the left column's middle: the left edge follows, and the
+ # rail is the seam cell left of where the rule will be.
+ for col in range(edge, edge - 11, -1):
+ mouse('motion', 1, col, 1)
+ require(rail(client) == edge - 11, f'resize rail at {rail(client)}, wanted {edge - 11}')
+ grab(root, 'grip-resize-drag')
+ mouse('up', 1, edge - 10, 1)
+ require(wait_for(lambda: rows(client)[1].index('New Tty', 10) - 3 == edge - 10), 'left edge did not follow the grip')
+ edge -= 10
+
+ # Over the left column's left half: it goes first, and the rail runs
+ # just past the left window edge's scroll rail.
+ mouse('down', 1, edge, 1)
+ for col in range(edge, 4, -5):
+ mouse('motion', 1, col, 10)
+ require(rail(client) == 1, f'reorder rail at {rail(client)}, wanted 1')
+ grab(root, 'grip-reorder-drag')
+ mouse('up', 1, 5, 10)
+ require(wait_for(lambda: 'alpha' in rows(client)[3][50:]), 'the column did not move first')
+ require(rail(client) is None, 'the rail outlived the drag')
+ grab(root, 'grip-done')
+ print('ok', root)
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('binary', type=Path)
+ parser.add_argument('output', type=Path)
+ parser.add_argument('--pad', type=int, default=0, help='leftover pixels past the grid (PARDES_TEST_PAD)')
+ run(parser.parse_args())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/snapshots/colgrip.golden b/test/snapshots/colgrip.golden
new file mode 100644
index 00000000..e096a665
--- /dev/null
+++ b/test/snapshots/colgrip.golden
@@ -0,0 +1,170 @@
+== style rest grid=100x24
+|0: 0-70 #b2c3b8,#1d2824, 71-99 d,#1d2824,
+|1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-49 d,#1d2824, 50-51 #ffffff,#887455, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d,
+|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-49 #b2c3b8,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-84 #dfbce5,#26362d, 85-99 #becfc4,#26362d,
+|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+== style held grid=100x24
+|1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-49 d,#1d2824, 50-51 #000000,#f4c086, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d,
+== snap resize-drag grid=100x24 cursor=54,5
+|Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill
+| New Tty Find Grep Joincol Delcol ╎ New Tty Find Grep Joincol Delcol
+| /tmp/pardes-snap/colgrip/cwd/w.txt S╎ve Tty Col /tmp/pardes-snap/colgrip/cwd Tty Save Mode Filt
+| 1 x ╎ ls
+| ╎ w.txt
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+| ╎
+== style resize-drag grid=100x24
+|1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-38 d,#1d2824, 39-39 #f4c086,#1d2824, 40-49 d,#1d2824, 50-51 #000000,#f4c086, 52-52 d,#26362d, 53-84 #becfc4,#26362d, 85-99 d,#26362d,
+|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-38 #b2c3b8,#1d2824, 39-39 #f4c086,#1d2824, 40-49 #b2c3b8,#1d2824, 50-51 #000000,#add38a, 52-52 d,#26362d, 53-81 #becfc4,#26362d, 82-84 #dfbce5,#26362d, 85-99 #becfc4,#26362d,
+|3: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|4: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-99 d,#0d1410,
+|6: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|7: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|8: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|9: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|10: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|11: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|12: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|13: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|14: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|15: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|16: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|17: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|18: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|19: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|22: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+|23: 0-0 d,#608269, 1-1 d,#0d1410, 2-6 #59675c,#0d1410, 7-38 d,#0d1410, 39-39 #f4c086,#0d1410, 40-49 d,#0d1410, 50-50 d,#608269, 51-51 d,#0d1410, 52-99 #e5eee5,#0d1410,
+== snap resize-done grid=100x24 cursor=44,5
+|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
+|2: /tmp/pardes-snap/colgrip/cwd/w.txt Sa /tmp/pardes-snap/colgrip/cwd Tty Save Mode Filter Collaps
+|3: 1 x ls
+|4: w.txt
+|5:
+|6:
+|7:
+|8:
+|9:
+|10:
+|11:
+|12:
+|13:
+|14:
+|15:
+|16:
+|17:
+|18:
+|19:
+|20:
+|21:
+|22:
+|23:
+== snap reorder-drag grid=100x24 cursor=44,5
+|3: ╎ 1 x ls
+|4: ╎ w.txt
+|5: ╎
+|6: ╎
+|7: ╎
+|8: ╎
+|9: ╎
+|10: ╎
+|11: ╎
+|12: ╎
+|13: ╎
+|14: ╎
+|15: ╎
+|16: ╎
+|17: ╎
+|18: ╎
+|19: ╎
+|20: ╎
+|21: ╎
+|22: ╎
+|23: ╎
+== style reorder-drag grid=100x24
+|1: 0-1 #ffffff,#887455, 2-2 d,#1d2824, 3-34 #b2c3b8,#1d2824, 35-39 d,#1d2824, 40-41 #000000,#f4c086, 42-42 d,#26362d, 43-74 #becfc4,#26362d, 75-99 d,#26362d,
+|2: 0-1 #ffffff,#496656, 2-2 d,#1d2824, 3-31 #b2c3b8,#1d2824, 32-36 #d3b0d9,#1d2824, 37-39 #b2c3b8,#1d2824, 40-41 #000000,#add38a, 42-42 d,#26362d, 43-71 #becfc4,#26362d, 72-74 #dfbce5,#26362d, 75-99 #becfc4,#26362d,
+|3: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-7 #e5eee5,#0d1410, 8-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|4: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|5: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-99 d,#0d1410,
+|6: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|7: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|8: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|9: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|10: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|11: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|12: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|13: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|14: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|15: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|16: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|17: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|18: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|19: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|20: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|21: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|22: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+|23: 0-0 d,#608269, 1-1 #f4c086,#0d1410, 2-6 #59675c,#0d1410, 7-39 d,#0d1410, 40-40 d,#608269, 41-41 d,#0d1410, 42-99 #e5eee5,#0d1410,
+== snap reorder-done grid=100x24 cursor=4,5
+|1: New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol
+|2: /tmp/pardes-snap/colgrip/cwd Tty Save Mode Filter Collaps /tmp/pardes-snap/colgrip/cwd/w.txt Sa
+|3: ls 1 x
+|4: w.txt
+|5:
+|6:
+|7:
+|8:
+|9:
+|10:
+|11:
+|12:
+|13:
+|14:
+|15:
+|16:
+|17:
+|18:
+|19:
+|20:
+|21:
+|22:
+|23:
diff --git a/test/snapshots/colgrip.snap b/test/snapshots/colgrip.snap
new file mode 100644
index 00000000..c2b403ed
--- /dev/null
+++ b/test/snapshots/colgrip.snap
@@ -0,0 +1,35 @@
+# Column grips: the two cells left of a column tag carry the column. A grabbed
+# grip lights, and a rail in its color stands where the column will land.
+# The right column is at x 50; its grip is cells 51-52 (1-based) on row 2.
+file w.txt x
+start 24 100 w.txt
+wait 8000 Newcol Joincol
+wait 8000 w.txt
+stable 700 20000
+key a-n
+key a-c
+stable 700 15000
+snapstyle rest
+# held on its own grip: lit, and no rail, since it lands where it is
+press left 51 2
+stable 400 5000
+snapstyle held
+# dropped short of the left column's middle, the left edge moves: the rail is
+# in the seam cell left of where the column's rule will be
+drag left 41 2
+stable 400 5000
+snap resize-drag
+snapstyle resize-drag
+release left 41 2
+stable 700 15000
+snap resize-done
+# carried over the left column's left half it goes first; the window edge
+# has no seam, so the rail runs in the gutter's last cell
+press left 41 2
+drag left 10 2
+stable 400 5000
+snap reorder-drag
+snapstyle reorder-drag
+release left 10 2
+stable 700 15000
+snap reorder-done