Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ fn buildBinaries(
"repeat",
"tcp-proxy",
"timeout",
"cowsay",
}) |name| {
try buildBinary(
b,
Expand Down
57 changes: 57 additions & 0 deletions docs/content/programs/cowsay.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#+TITLE: cowsay
#+DATE: 2026-03-16T04:25:00+0800
#+LASTMOD: 2026-03-16T04:25:00+0800
#+TYPE: docs
#+DESCRIPTION: Generate an ASCII picture of a cow saying something

#+begin_src bash :results verbatim :exports results :wrap example :dir ../../..
./zig-out/bin/cowsay -h
#+end_src

#+RESULTS:
#+begin_example
USAGE:
./zig-out/bin/cowsay [OPTIONS] [--] [message]

OPTIONS:
-f, --face STRING Which cow face to use (cow, tux). Default: cow.(valid: cow|tux)(default: cow)
-v, --version Print version.
-h, --help Print help information.
#+end_example

** Demo
#+begin_src bash
$ cowsay hi
____
< hi >
----
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||

$ cowsay -f tux hi
____
< hi >
----
\
\
.--.
|o_o |
|:_/ |
// \ \
(| | )
/'\_ _/`\
\___)=(___/

$ cowsay hello world
___________
< hello world >
-----------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
#+end_src
190 changes: 190 additions & 0 deletions src/bin/cowsay.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
//! Cowsay in Zig
//! https://en.wikipedia.org/wiki/Cowsay

const std = @import("std");
const simargs = @import("simargs");
const util = @import("util.zig");
const mem = std.mem;
const testing = std.testing;

// The default ASCII cow art.
const cow_art: []const u8 =
\\ \ ^__^
\\ \ (oo)\_______
\\ (__)\ )\/\
\\ ||----w |
\\ || ||
\\
;

// The Tux (Linux penguin) ASCII art.
const tux_art: []const u8 =
\\ \
\\ \
\\ .--.
\\ |o_o |
\\ |:_/ |
\\ // \ \
\\ (| | )
\\ /'\_ _/`\
\\ \___)=(___/
\\
;

const CowFace = enum {
cow,
tux,
};

pub fn main() !void {
var gpa = util.Allocator.instance;
defer gpa.deinit();
const allocator = gpa.allocator();

const opt = try simargs.parse(allocator, struct {
face: CowFace = .cow,
help: bool = false,
version: bool = false,

pub const __shorts__ = .{
.face = .f,
.help = .h,
.version = .v,
};

pub const __messages__ = .{
.face = "Which cow face to use (cow, tux). Default: cow.",
.help = "Print help information.",
.version = "Print version.",
};
}, .{
.argument_prompt = "[message]",
.version_string = util.get_build_info(),
});
defer opt.deinit();

// Join all positional arguments with spaces to form the message.
var message_parts_buf: [4096]u8 = undefined;
const message: []const u8 = if (opt.positional_arguments.len == 0)
""
else blk: {
var fbs = std.io.fixedBufferStream(&message_parts_buf);
const fbs_writer = fbs.writer();
for (opt.positional_arguments, 0..) |arg, i| {
if (i > 0) try fbs_writer.writeByte(' ');
try fbs_writer.writeAll(arg);
}
break :blk fbs.getWritten();
};

const stdout = std.fs.File.stdout();
var output_buf: [8192]u8 = undefined;
var writer = stdout.writer(&output_buf);

try writeSpeechBubble(&writer.interface, message);

const art = switch (opt.options.face) {
.cow => cow_art,
.tux => tux_art,
};
try writer.interface.writeAll(art);
try writer.interface.flush();
}

/// Renders a speech bubble around the given message to the writer.
/// Single-line messages use `< text >` borders.
/// Multi-line messages use `/`, `|`, `\` borders on the sides.
fn writeSpeechBubble(writer: anytype, message: []const u8) !void {
// Collect lines and find the maximum line width.
var lines_buf: [64][]const u8 = undefined;
var line_count: usize = 0;
var max_width: usize = 0;

var iter = mem.splitScalar(u8, message, '\n');
while (iter.next()) |line| {
if (line_count >= lines_buf.len) break;
lines_buf[line_count] = line;
line_count += 1;
if (line.len > max_width) {
max_width = line.len;
}
}

const lines = lines_buf[0..line_count];
// The border width includes one space of padding on each side.
const border_width = max_width + 2;

// Write the top border: a space then `border_width` underscores.
try writer.writeAll(" ");
for (0..border_width) |_| try writer.writeAll("_");
try writer.writeAll("\n");

// Write message lines with box-drawing characters.
if (lines.len == 1) {
// Single line: use angle brackets.
try writer.writeAll("< ");
try writer.writeAll(lines[0]);
try writer.writeAll(" >\n");
} else {
for (lines, 0..) |line, i| {
const left = if (i == 0) "/ " else if (i == lines.len - 1) "\\ " else "| ";
const right = if (i == 0) " \\" else if (i == lines.len - 1) " /" else " |";
try writer.writeAll(left);
try writer.writeAll(line);
// Pad shorter lines to the maximum width so all borders align.
for (0..max_width - line.len) |_| try writer.writeAll(" ");
try writer.writeAll(right);
try writer.writeAll("\n");
}
}

// Write the bottom border: a space then `border_width` dashes.
try writer.writeAll(" ");
for (0..border_width) |_| try writer.writeAll("-");
try writer.writeAll("\n");
}

test "speech bubble single line" {
// A single-line message should use '< text >' delimiters.
var out: std.ArrayList(u8) = .empty;
defer out.deinit(testing.allocator);
try writeSpeechBubble(out.writer(testing.allocator), "hi");

try testing.expectEqualStrings(
\\ ____
\\< hi >
\\ ----
\\
, out.items);
}

test "speech bubble multi line" {
// A multi-line message should use '/', '|', '\' delimiters.
var out: std.ArrayList(u8) = .empty;
defer out.deinit(testing.allocator);
try writeSpeechBubble(out.writer(testing.allocator), "hello\nworld");

try testing.expectEqualStrings(
\\ _______
\\/ hello \
\\\ world /
\\ -------
\\
, out.items);
}

test "speech bubble three lines" {
// Middle lines should use '|' delimiters; first is '/', last is '\'.
var out: std.ArrayList(u8) = .empty;
defer out.deinit(testing.allocator);
try writeSpeechBubble(out.writer(testing.allocator), "one\ntwo\nthree");

try testing.expectEqualStrings(
\\ _______
\\/ one \
\\| two |
\\\ three /
\\ -------
\\
, out.items);
}