summaryrefslogtreecommitdiff
path: root/src/ninep/cols.zig
blob: e168851767ddfe9d6444e998d6f8158d9f77bbb9 (plain) (blame)
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
//! The layout and the header tags: /layout, /tag (the workspace's) and
//! /col/<n>/tag (each column's). 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 index, 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 <index>`.
pub fn writeLayout(p: *const Pardes, w: *std.Io.Writer) !void {
    const active = activeColumn(p);
    for (0..p.ncol) |c| {
        try w.print("{d} {d} {d} {s} {s}", .{
            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 (active) |c| try w.print("active {d}\n", .{c}) else try w.writeAll("active -\n");
}

pub fn layoutLen(p: *const 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();
}

/// The header a node names: the workspace's (`col` null) or a column's.
fn header(p: *Pardes, col: ?usize) ?*Text {
    const c = col orelse return &p.global_tag;
    return if (c < p.ncol) &p.col_tags[c] else null;
}

pub fn headerText(p: *Pardes, col: ?usize) ?[]const u8 {
    const c = col orelse return tagline.topbar(p);
    return if (c < p.ncol) tagline.columnTag(p, c) else null;
}

pub fn read(p: *Pardes, req: Req, col: ?usize) Reply {
    const text = headerText(p, col) 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, col: ?usize) Reply {
    const t = header(p, col) orelse return Reply.fail(req.tag, E.NOENT);
    if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 };
    const was = headerText(p, col).?;
    var data = req.data;
    const rewriting = p.fs.header_rewrite == rewriteKey(col);
    // 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, col: ?usize) tree.Status {
    const t = header(p, col) orelse return .err;
    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(col);
    p.fs.header_held = false;
    return .ok;
}

/// Which header a truncation was of: the workspace's is 0, a column's its
/// index + 1.
fn rewriteKey(col: ?usize) u32 {
    return if (col) |c| @intCast(c + 1) else 0;
}

pub fn released(p: *Pardes, col: ?usize) void {
    if (p.fs.header_rewrite != rewriteKey(col)) 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 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, "0 {d} {d} notcurrent full {d}\n", .{ 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, "active 1\n"));
    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, last.
    try testing.expect(std.mem.endsWith(u8, th.rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes, " 0\n"));
    try testing.expect(th.logHas(p, "newcol 1\n"));
}

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);
    try testing.expectEqual(tree.Status.ok, th.look_up(p, col_dir, "0").reply.status);
    try testing.expectEqual(E.NOENT, th.look_up(p, col_dir, "7").errno());
    const tag = Node.ofCol(0, .tag);
    try testing.expectEqual(tree.Status.ok, th.look_up(p, Node.ofCol(0, .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));
}