// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! `--line-prefix=`: a writer putting the prefix before every //! line written through it. git was seen to put it before every line of //! `diff`, `diff-tree` and `diff-index` output and, in `log` and `show`, //! of the commits too, blank lines and the separator included, without //! narrowing `--stat` to make room for it. const std = @import("std"); const Io = std.Io; /// Writes to `out`, with `prefix` before each line. pub const LinePrefix = struct { out: *Io.Writer, prefix: []const u8, at_start: bool = true, writer: Io.Writer, /// A writer onto `out`; `buffer` is its own. pub fn init(out: *Io.Writer, prefix: []const u8, buffer: []u8) LinePrefix { return .{ .out = out, .prefix = prefix, .writer = .{ .vtable = &.{ .drain = drain }, .buffer = buffer } }; } /// Starts a line of its own: the prefix, if nothing has been written /// on the line, even if nothing follows it. git was seen to write the /// prefix before a commit's description however little that writes. pub fn startLine(lp: *LinePrefix) Io.Writer.Error!void { try lp.writer.flush(); if (lp.at_start) try lp.out.writeAll(lp.prefix); lp.at_start = false; } /// Ends what was written, so that the next thing written has the /// prefix before it, as git was seen to put it before a diff after a /// commit's description, even where the description did not end its /// line. pub fn endLine(lp: *LinePrefix) Io.Writer.Error!void { try lp.writer.flush(); lp.at_start = true; } fn emit(lp: *LinePrefix, bytes: []const u8) Io.Writer.Error!void { var rest = bytes; while (rest.len != 0) { if (lp.at_start) { try lp.out.writeAll(lp.prefix); lp.at_start = false; } const end = if (std.mem.findScalar(u8, rest, '\n')) |nl| nl + 1 else rest.len; try lp.out.writeAll(rest[0..end]); if (rest[end - 1] == '\n') lp.at_start = true; rest = rest[end..]; } } fn drain(w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { const lp: *LinePrefix = @fieldParentPtr("writer", w); try lp.emit(w.buffer[0..w.end]); w.end = 0; var n: usize = 0; for (data[0 .. data.len - 1]) |d| { try lp.emit(d); n += d.len; } const last = data[data.len - 1]; for (0..splat) |_| try lp.emit(last); n += last.len * splat; return n; } }; /// The prefix `--line-prefix` gives among `args`, the last if more than /// one, looking no further than `--`. pub fn find(args: []const []const u8) ?[]const u8 { var found: ?[]const u8 = null; var i: usize = 0; while (i < args.len) : (i += 1) { const a = args[i]; if (std.mem.eql(u8, a, "--")) break; if (std.mem.startsWith(u8, a, "--line-prefix=")) { found = a["--line-prefix=".len..]; } else if (std.mem.eql(u8, a, "--line-prefix") and i + 1 < args.len) { found = args[i + 1]; i += 1; } } return found; } test "every line is prefixed" { var out: Io.Writer.Allocating = .init(std.testing.allocator); defer out.deinit(); var buf: [4]u8 = undefined; var lp: LinePrefix = .init(&out.writer, ">> ", &buf); try lp.writer.writeAll("one\n\ntwo"); try lp.writer.print(" and {d}\n", .{3}); try lp.writer.flush(); try std.testing.expectEqualStrings(">> one\n>> \n>> two and 3\n", out.written()); }