summaryrefslogtreecommitdiff
path: root/src/message.zig
blob: 0855c448f3d3cf843b1f532034ca2ef21ced815a (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
//! Native-shell ownership of the transient message row's one clock read.
//!
//! The core owns the row, the buffer and when it goes away (Pardes.setMessage);
//! what it does not own is a clock, and its header says so. So what happened is
//! narrated HERE — at the moment the IO it narrates actually finished, which is
//! also the only moment it is true: a save that failed says nothing — and handed
//! over as finished text. Both native shells post the same two events (a save,
//! an external reload), so the wording sits in one place instead of once per
//! shell, where the two copies would drift the first time either was reworded.
//! Same split, and the same reason, as temp_file.zig owning `New`'s mkstemp.
//!
//! LOCAL time, not UTC: this row is read by a person sitting in front of the
//! terminal. dump.zig stamps filenames in UTC because those are sorted, not
//! read.
const std = @import("std");
const libc = std.c;

/// glibc/musl/macOS `struct tm`. Only the first three fields are ever read; the
/// rest are declared because localtime_r writes the whole struct.
const Tm = extern struct {
    sec: c_int,
    min: c_int,
    hour: c_int,
    mday: c_int,
    mon: c_int,
    year: c_int,
    wday: c_int,
    yday: c_int,
    isdst: c_int,
    gmtoff: c_long,
    zone: ?[*:0]const u8,
};
extern "c" fn localtime_r(timep: *const libc.time_t, result: *Tm) ?*Tm;

/// `HH:MM:SS  <verb> <subject>` into `buf`, e.g. `14:32:07  saved /etc/hosts`.
/// Written into a caller buffer rather than allocated, because the core's own
/// message buffer is fixed too and a message wider than a pane is one nobody
/// reads. A subject too long for the room left is cut from the FRONT: the tail
/// of a path is the part that identifies it.
///
/// $PARDES_NOTIME blanks the DIGITS and nothing else. The snapshot harness sets
/// it (the same flag hides the language backend's durations) because a wall
/// clock cannot live in a golden — but a message that vanished under the
/// harness could not be pinned at all, so the row still renders, still says
/// what happened, and still occupies the same columns.
/// The stamped text WITHOUT its clock — what two of these rows have in common
/// when they say the same thing at different times.
///
/// The message log de-duplicates on this rather than on the whole row, because
/// the clock makes every host message unique by construction: `saved x` at
/// 14:32:07 and at 14:32:09 are different strings, so a watched file rebuilt
/// in a loop filled the ring with identical-looking rows, each counted once.
/// That is exactly the case the de-duplication exists for.
pub fn body(text: []const u8) []const u8 {
    // `HH:MM:SS  ` — ten bytes, digits or the `--:--:--` the harness blanks
    // them to. Anything else is a message that was never stamped.
    if (text.len < 10) return text;
    if (text[2] != ':' or text[5] != ':' or text[8] != ' ' or text[9] != ' ') return text;
    for ([_]usize{ 0, 1, 3, 4, 6, 7 }) |i| {
        if (!std.ascii.isDigit(text[i]) and text[i] != '-') return text;
    }
    return text[10..];
}

pub fn stamp(buf: []u8, verb: []const u8, subject: []const u8) []const u8 {
    var clock: [8]u8 = "--:--:--".*;
    const notime = if (libc.getenv("PARDES_NOTIME")) |v| std.mem.span(v).len != 0 else false;
    if (!notime) {
        var ts: libc.timespec = undefined;
        _ = libc.clock_gettime(.REALTIME, &ts);
        const secs: libc.time_t = ts.sec;
        var tm: Tm = undefined;
        if (localtime_r(&secs, &tm) != null) {
            // unsigned on purpose: Zig 0.16 prints a `+` for a positive SIGNED
            // int, and `{d:0>2}` on a c_int would spell 14:32:07 as +1:+3:+7
            _ = std.fmt.bufPrint(&clock, "{d:0>2}:{d:0>2}:{d:0>2}", .{
                @as(u32, @intCast(tm.hour)),
                @as(u32, @intCast(tm.min)),
                @as(u32, @intCast(tm.sec)),
            }) catch {};
        }
    }
    const head = std.fmt.bufPrint(buf, "{s}  {s} ", .{ &clock, verb }) catch return buf[0..0];
    const room = buf.len - head.len;
    const tail = if (subject.len <= room) subject else subject[subject.len - room ..];
    @memcpy(buf[head.len..][0..tail.len], tail);
    return buf[0 .. head.len + tail.len];
}