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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
|
//! The layout and the header tags: /layout, /tag (the workspace's) and
//! /col/<serial>/tag (each column's; a column's serial stays while it lives,
//! as a pane's does). acme serves no column files -- its columns
//! are only a window's place -- so these go past it: a script can see where
//! the panes are and edit the tags a person clicks in.
const std = @import("std");
const pardes = @import("../pardes.zig");
const Pardes = pardes.Pardes;
const tree = @import("tree.zig");
const tagline = pardes.tagline;
const layout = pardes.layout;
const Text = pardes.panes.Text;
const Req = tree.Req;
const Reply = tree.Reply;
const E = tree.E;
/// The column with the keyboard: the header's, when a column tag has it,
/// else the active pane's. Null when the workspace tag has it and no pane
/// is active.
pub fn activeColumn(p: *const Pardes) ?usize {
if (p.header_focus) if (p.header_column) |c| return if (c < p.ncol) c else null;
const at = layout.findPane(p, p.active) orelse return null;
return at.col;
}
/// One line a column: its serial (the name /col/<serial> and the log give
/// it, which stays while it lives), its index left to right, x and width in
/// cells, `current` or `notcurrent` (rio's words, for the column with the
/// keyboard), `empty` or `full`, and its panes' serials, top to bottom;
/// then `active <serial>`: the active column, where pane/new and a look
/// place a pane next (acme's activecol), not always the keyboard's.
pub fn writeLayout(p: *Pardes, w: *std.Io.Writer) !void {
const active = activeColumn(p);
for (0..p.ncol) |c| {
try w.print("{d} {d} {d} {d} {s} {s}", .{
layout.columnSerial(p, c), c, p.col_x[c], p.col_w[c],
if (active == c) "current" else "notcurrent", if (p.col_n[c] == 0) "empty" else "full",
});
for (p.col_panes[c][0..p.col_n[c]]) |id| if (p.panes[id]) |pane| try w.print(" {d}", .{pane.serial});
try w.writeByte('\n');
}
if (p.activeColumn() orelse active) |c| try w.print("active {d}\n", .{layout.columnSerial(p, c)}) else try w.writeAll("active -\n");
}
pub fn layoutLen(p: *Pardes) u64 {
var buf: [256]u8 = undefined;
var counting: std.Io.Writer.Discarding = .init(&buf);
writeLayout(p, &counting.writer) catch return 0;
return counting.fullCount();
}
/// A header by its node: the workspace's (`serial` null) or the column's
/// with that serial; null when that column has closed.
const Header = struct { text: *Text, col: ?usize };
fn header(p: *Pardes, serial: ?u32) ?Header {
const s = serial orelse return .{ .text = &p.global_tag, .col = null };
const c = layout.columnBySerial(p, s) orelse return null;
return .{ .text = &p.col_tags[c], .col = c };
}
pub fn headerText(p: *Pardes, serial: ?u32) ?[]const u8 {
const h = header(p, serial) orelse return null;
return if (h.col) |c| tagline.columnTag(p, c) else tagline.topbar(p);
}
pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply {
const text = headerText(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
const out = p.fs.stage(p.gpa);
out.appendSlice(p.gpa, text) catch return Reply.fail(req.tag, E.NOMEM);
out.append(p.gpa, '\n') catch return Reply.fail(req.tag, E.NOMEM);
return tree.stagedReply(p, req);
}
/// A write appends, as a pane tag's does; the tag is one line, so a newline
/// in it becomes a space (tagline.flattenHeaders), but after a truncation
/// the newline that ends the write is dropped, as a pane tag drops it.
pub fn write(p: *Pardes, req: Req, serial: ?u32) Reply {
const t = (header(p, serial) orelse return Reply.fail(req.tag, E.NOENT)).text;
if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 };
const was = headerText(p, serial).?;
var data = req.data;
const rewriting = p.fs.header_rewrite == rewriteKey(serial);
// A newline held from the write before goes in once more text follows.
const held: []const u8 = if (rewriting and p.fs.header_held) " " else "";
p.fs.header_held = false;
if (rewriting and data[data.len - 1] == '\n') {
data = data[0 .. data.len - 1];
p.fs.header_held = true;
}
const next = std.mem.concat(p.gpa, u8, &.{ was, held, data }) catch return Reply.fail(req.tag, E.NOMEM);
std.mem.replaceScalar(u8, next, '\n', ' ');
if (t.own) |own| p.gpa.free(own);
t.own = next;
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
/// Truncating clears it, as a pane tag's `cleartag`: its default words go
/// too, and `u` in it brings them back.
pub fn truncate(p: *Pardes, serial: ?u32) tree.Status {
const t = (header(p, serial) orelse return .err).text;
const empty = p.gpa.alloc(u8, 0) catch return .err;
t.remember(p.gpa, if (t.own) |own| .{ .text = own } else null);
if (t.own) |own| p.gpa.free(own);
t.own = empty;
t.cur_row = 0;
t.cur_col = 0;
p.fs.header_rewrite = rewriteKey(serial);
p.fs.header_held = false;
return .ok;
}
/// Which header a truncation was of: the workspace's is 0 (no column's
/// serial is), a column's its serial.
fn rewriteKey(serial: ?u32) u32 {
return serial orelse 0;
}
pub fn released(p: *Pardes, serial: ?u32) void {
if (p.fs.header_rewrite != rewriteKey(serial)) return;
p.fs.header_rewrite = null;
p.fs.header_held = false;
}
// ---- tests ----
const testing = std.testing;
const th = @import("testing.zig");
const Node = tree.Node;
test "layout lists each column's place, panes and state, and the active one" {
const p = try th.withFile(testing.allocator, "x\n");
defer p.deinit();
p.newColumn(p.active);
p.sync();
const first_serial = layout.columnSerial(p, 0);
const new_serial = layout.columnSerial(p, 1);
try testing.expect(first_serial != new_serial);
const layout_node = @intFromEnum(tree.TopFile.layout);
const text = th.rd(p, layout_node, 0, 4096).bytes;
var want: [256]u8 = undefined;
const first = try std.fmt.bufPrint(&want, "{d} 0 {d} {d} notcurrent full {d}\n", .{ first_serial, p.col_x[0], p.col_w[0], p.panes[0].?.serial });
try testing.expect(std.mem.startsWith(u8, text, first));
// Newcol gives the new, empty column's tag the keyboard.
try testing.expect(std.mem.indexOf(u8, text, "current empty\n") != null);
try testing.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "active {d}\n", .{new_serial})));
try testing.expectEqual(@as(u64, text.len), th.call(p, .{ .tag = 1, .op = .getattr, .node = layout_node }).reply.attr.size);
// /index says which column each pane is in, by its serial, last.
try testing.expect(std.mem.endsWith(u8, th.rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, try std.fmt.bufPrint(&want, " {d}\n", .{first_serial})));
try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "newcol {d}\n", .{new_serial})));
// A column that closes takes its name with it; the others keep theirs.
try p.removeColumn(1);
try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "delcol {d}\n", .{new_serial})));
try testing.expectEqual(E.NOENT, th.look_up(p, @intFromEnum(tree.TopFile.col), try std.fmt.bufPrint(&want, "{d}", .{new_serial})).errno());
try testing.expectEqual(first_serial, layout.columnSerial(p, 0));
}
test "a column's tag and the workspace's read, replace and append as a pane tag does, on one line" {
const p = try th.withFile(testing.allocator, "x\n");
defer p.deinit();
const col_dir = @intFromEnum(tree.TopFile.col);
const serial = layout.columnSerial(p, 0);
var name: [16]u8 = undefined;
try testing.expectEqual(tree.Status.ok, th.look_up(p, col_dir, try std.fmt.bufPrint(&name, "{d}", .{serial})).reply.status);
try testing.expectEqual(E.NOENT, th.look_up(p, col_dir, "0").errno());
try testing.expectEqual(E.NOENT, th.look_up(p, col_dir, "777").errno());
const tag = Node.ofCol(serial, .tag);
try testing.expectEqual(tree.Status.ok, th.look_up(p, Node.ofCol(serial, .dir), "tag").reply.status);
try testing.expectEqualStrings("New Tty Find Grep Joincol Delcol\n", th.rd(p, tag, 0, 4096).bytes);
// `echo Make > tag`: cleared, then the newline ending the write dropped.
_ = th.call(p, .{ .tag = 1, .op = .setattr, .node = tag, .truncate = true });
_ = th.wr(p, tag, "Make\n");
try testing.expectEqualStrings("Make\n", th.rd(p, tag, 0, 4096).bytes);
_ = th.call(p, .{ .tag = 2, .op = .release, .node = tag });
// `>>` appends; a newline in a one-line tag is a space.
_ = th.wr(p, tag, " Lint\nNow");
try testing.expectEqualStrings("Make Lint Now\n", th.rd(p, tag, 0, 4096).bytes);
const top = @intFromEnum(tree.TopFile.tag);
_ = th.call(p, .{ .tag = 3, .op = .setattr, .node = top, .truncate = true });
_ = th.wr(p, top, "Newcol Exit\n");
try testing.expectEqualStrings("Newcol Exit", tagline.topbar(p));
}
test "a focus write makes its pane's column the active one: layout says so and pane/new goes there" {
for ([_][]const u8{ "Placement acme", "Placement pardes" }) |placement| {
const p = try th.withFile(testing.allocator, "x\n");
defer p.deinit();
try testing.expect(p.executeBuiltinLine(p.active, placement));
const left = p.panes[0].?.serial;
p.newColumn(p.active);
p.newScratchFor(p.active, .fs); // fills the new, right column
p.sync();
try testing.expectEqual(@as(usize, 1), layout.findPane(p, p.active).?.col);
var digits: [16]u8 = undefined;
_ = th.wr(p, @intFromEnum(tree.TopFile.focus), try std.fmt.bufPrint(&digits, "{d}\n", .{left}));
var want: [32]u8 = undefined;
const text = th.rd(p, @intFromEnum(tree.TopFile.layout), 0, 4096).bytes;
try testing.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "active {d}\n", .{layout.columnSerial(p, 0)})));
const made = th.call(p, .{ .tag = 1, .op = .open, .node = @intFromEnum(tree.TopFile.new) }).reply.handle;
try testing.expectEqual(@as(usize, 0), layout.findPane(p, p.paneBySerial(made).?).?.col);
}
}
|