chore: Initial commit
This commit is contained in:
parent
5ec75c4999
commit
70f2fc619d
12 changed files with 1135 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
zig-out
|
||||
zig-cache
|
91
build.zig
Normal file
91
build.zig
Normal file
|
@ -0,0 +1,91 @@
|
|||
const std = @import("std");
|
||||
|
||||
// Although this function looks imperative, note that its job is to
|
||||
// declaratively construct a build graph that will be executed by an external
|
||||
// runner.
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "zpinner",
|
||||
// In this case the main source file is merely a path, however, in more
|
||||
// complicated build scripts, this could be a generated file.
|
||||
.root_source_file = b.path("src/zpinner.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// This declares intent for the library to be installed into the standard
|
||||
// location when the user invokes the "install" step (the default step when
|
||||
// running `zig build`).
|
||||
b.installArtifact(lib);
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "zpinner",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// This declares intent for the executable to be installed into the
|
||||
// standard location when the user invokes the "install" step (the default
|
||||
// step when running `zig build`).
|
||||
b.installArtifact(exe);
|
||||
|
||||
// This *creates* a Run step in the build graph, to be executed when another
|
||||
// step is evaluated that depends on it. The next line below will establish
|
||||
// such a dependency.
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
|
||||
// By making the run step depend on the install step, it will be run from the
|
||||
// installation directory rather than directly from within the cache directory.
|
||||
// This is not necessary, however, if the application depends on other installed
|
||||
// files, this ensures they will be present and in the expected location.
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
// This allows the user to pass arguments to the application in the build
|
||||
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||
// and can be selected like this: `zig build run`
|
||||
// This will evaluate the `run` step rather than the default, which is "install".
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// Creates a step for unit testing. This only builds the test executable
|
||||
// but does not run it.
|
||||
const lib_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
|
||||
const exe_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
|
||||
|
||||
// Similar to creating the run step earlier, this exposes a `test` step to
|
||||
// the `zig build --help` menu, providing a way for the user to request
|
||||
// running the unit tests.
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_lib_unit_tests.step);
|
||||
test_step.dependOn(&run_exe_unit_tests.step);
|
||||
}
|
31
build.zig.zon
Normal file
31
build.zig.zon
Normal file
|
@ -0,0 +1,31 @@
|
|||
.{
|
||||
.name = "zpinner",
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
// In a future version of Zig it will be used for package deduplication.
|
||||
.version = "0.0.0",
|
||||
|
||||
// This field is optional.
|
||||
// This is currently advisory only; Zig does not yet do anything
|
||||
// with this value.
|
||||
//.minimum_zig_version = "0.11.0",
|
||||
|
||||
// This field is optional.
|
||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||
// Once all dependencies are fetched, `zig build` no longer requires
|
||||
// internet connectivity.
|
||||
.dependencies = .{},
|
||||
.paths = .{
|
||||
// This makes *all* files, recursively, included in this package. It is generally
|
||||
// better to explicitly list the files and directories instead, to insure that
|
||||
// fetching from tarballs, file system paths, and version control all result
|
||||
// in the same contents hash.
|
||||
"",
|
||||
// For example...
|
||||
//"build.zig",
|
||||
//"build.zig.zon",
|
||||
//"src",
|
||||
//"LICENSE",
|
||||
//"README.md",
|
||||
},
|
||||
}
|
3
src/ansi.zig
Normal file
3
src/ansi.zig
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub const Style = @import("style.zig").Style;
|
||||
pub const Color = @import("style.zig").Color;
|
||||
pub const format = @import("format.zig");
|
4
src/characters.zig
Normal file
4
src/characters.zig
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub const Moon = "🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘";
|
||||
pub const Snake = "⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏";
|
||||
pub const SnakeLoad = "⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷";
|
||||
pub const Earth = "🌍 🌎 🌏";
|
103
src/cursor.zig
Normal file
103
src/cursor.zig
Normal file
|
@ -0,0 +1,103 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const fixedBufferStream = std.io.fixedBufferStream;
|
||||
const esc = "\x1B";
|
||||
const csi = esc ++ "[";
|
||||
|
||||
pub const CursorMode = enum(u8) {
|
||||
blinking_block = 1,
|
||||
block,
|
||||
blinking_underscore,
|
||||
underscore,
|
||||
blinking_I_beam,
|
||||
I_beam,
|
||||
};
|
||||
|
||||
pub fn setCursorMode(writer: anytype, mode: CursorMode) !void {
|
||||
const modeNumber = @intFromEnum(mode);
|
||||
try writer.print(csi ++ "{d} q", .{modeNumber});
|
||||
}
|
||||
|
||||
pub fn hideCursor(writer: anytype) !void {
|
||||
try writer.writeAll(csi ++ "?25l");
|
||||
}
|
||||
|
||||
pub fn showCursor(writer: anytype) !void {
|
||||
try writer.writeAll(csi ++ "?25h");
|
||||
}
|
||||
|
||||
pub fn saveCursor(writer: anytype) !void {
|
||||
try writer.writeAll(csi ++ "s");
|
||||
}
|
||||
|
||||
pub fn restoreCursor(writer: anytype) !void {
|
||||
try writer.writeAll(csi ++ "u");
|
||||
}
|
||||
|
||||
pub fn setCursor(writer: anytype, x: usize, y: usize) !void {
|
||||
try writer.print(csi ++ "{};{}H", .{ y + 1, x + 1 });
|
||||
}
|
||||
|
||||
pub fn setCursorRow(writer: anytype, row: usize) !void {
|
||||
try writer.print(csi ++ "{}H", .{row + 1});
|
||||
}
|
||||
|
||||
pub fn setCursorColumn(writer: anytype, column: usize) !void {
|
||||
try writer.print(csi ++ "{}G", .{column + 1});
|
||||
}
|
||||
|
||||
pub fn cursorUp(writer: anytype, lines: usize) !void {
|
||||
try writer.print(csi ++ "{}A", .{lines});
|
||||
}
|
||||
|
||||
pub fn cursorDown(writer: anytype, lines: usize) !void {
|
||||
try writer.print(csi ++ "{}B", .{lines});
|
||||
}
|
||||
|
||||
pub fn cursorForward(writer: anytype, columns: usize) !void {
|
||||
try writer.print(csi ++ "{}C", .{columns});
|
||||
}
|
||||
|
||||
pub fn cursorBackward(writer: anytype, columns: usize) !void {
|
||||
try writer.print(csi ++ "{}D", .{columns});
|
||||
}
|
||||
|
||||
pub fn cursorNextLine(writer: anytype, lines: usize) !void {
|
||||
try writer.print(csi ++ "{}E", .{lines});
|
||||
}
|
||||
|
||||
pub fn cursorPreviousLine(writer: anytype, lines: usize) !void {
|
||||
try writer.print(csi ++ "{}F", .{lines});
|
||||
}
|
||||
|
||||
pub fn scrollUp(writer: anytype, lines: usize) !void {
|
||||
try writer.print(csi ++ "{}S", .{lines});
|
||||
}
|
||||
|
||||
pub fn scrollDown(writer: anytype, lines: usize) !void {
|
||||
try writer.print(csi ++ "{}T", .{lines});
|
||||
}
|
||||
|
||||
test "test cursor mode BLINKING_UNDERSCORE" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try setCursorMode(fixed_buf_stream.writer(), .blinking_underscore);
|
||||
// the space is needed
|
||||
const expected = csi ++ "3 q";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "test cursor mode BLINKING_I_BEAM" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try setCursorMode(fixed_buf_stream.writer(), .blinking_I_beam);
|
||||
// the space is needed
|
||||
const expected = csi ++ "5 q";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
304
src/format.zig
Normal file
304
src/format.zig
Normal file
|
@ -0,0 +1,304 @@
|
|||
const std = @import("std");
|
||||
const fixedBufferStream = std.io.fixedBufferStream;
|
||||
const testing = std.testing;
|
||||
|
||||
const style = @import("style.zig");
|
||||
const Style = style.Style;
|
||||
const FontStyle = style.FontStyle;
|
||||
const Color = style.Color;
|
||||
|
||||
const esc = "\x1B";
|
||||
const csi = esc ++ "[";
|
||||
|
||||
const reset = csi ++ "0m";
|
||||
|
||||
const font_style_codes = std.ComptimeStringMap([]const u8, .{
|
||||
.{ "bold", "1" },
|
||||
.{ "dim", "2" },
|
||||
.{ "italic", "3" },
|
||||
.{ "underline", "4" },
|
||||
.{ "slowblink", "5" },
|
||||
.{ "rapidblink", "6" },
|
||||
.{ "reverse", "7" },
|
||||
.{ "hidden", "8" },
|
||||
.{ "crossedout", "9" },
|
||||
.{ "fraktur", "20" },
|
||||
.{ "overline", "53" },
|
||||
});
|
||||
|
||||
/// Update the current style of the ANSI terminal
|
||||
///
|
||||
/// Optionally accepts the previous style active on the
|
||||
/// terminal. Using this information, the function will update only
|
||||
/// the attributes which are new in order to minimize the amount
|
||||
/// written.
|
||||
///
|
||||
/// Tries to use as little bytes as necessary. Use this function if
|
||||
/// you want to optimize for smallest amount of transmitted bytes
|
||||
/// instead of computation speed.
|
||||
pub fn updateStyle(writer: anytype, new: Style, old: ?Style) !void {
|
||||
if (old) |sty| if (new.eql(sty)) return;
|
||||
if (new.isDefault()) return try resetStyle(writer);
|
||||
|
||||
// A reset is required if the new font style has attributes not
|
||||
// present in the old style or if the old style is not known
|
||||
const reset_required = if (old) |sty| !sty.font_style.subsetOf(new.font_style) else true;
|
||||
if (reset_required) try resetStyle(writer);
|
||||
|
||||
// Start the escape sequence
|
||||
try writer.writeAll(csi);
|
||||
var written_something = false;
|
||||
|
||||
// Font styles
|
||||
const write_styles = if (reset_required) new.font_style else new.font_style.without(old.?.font_style);
|
||||
inline for (std.meta.fields(FontStyle)) |field| {
|
||||
if (@field(write_styles, field.name)) {
|
||||
const code = font_style_codes.get(field.name).?;
|
||||
if (written_something) {
|
||||
try writer.writeAll(";");
|
||||
} else {
|
||||
written_something = true;
|
||||
}
|
||||
try writer.writeAll(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Foreground color
|
||||
if (reset_required and new.foreground != .Default or old != null and !old.?.foreground.eql(new.foreground)) {
|
||||
if (written_something) {
|
||||
try writer.writeAll(";");
|
||||
} else {
|
||||
written_something = true;
|
||||
}
|
||||
|
||||
switch (new.foreground) {
|
||||
.Default => try writer.writeAll("39"),
|
||||
.Black => try writer.writeAll("30"),
|
||||
.Red => try writer.writeAll("31"),
|
||||
.Green => try writer.writeAll("32"),
|
||||
.Yellow => try writer.writeAll("33"),
|
||||
.Blue => try writer.writeAll("34"),
|
||||
.Magenta => try writer.writeAll("35"),
|
||||
.Cyan => try writer.writeAll("36"),
|
||||
.White => try writer.writeAll("37"),
|
||||
.Fixed => |fixed| try writer.print("38;5;{}", .{fixed}),
|
||||
.Grey => |grey| try writer.print("38;2;{};{};{}", .{ grey, grey, grey }),
|
||||
.RGB => |rgb| try writer.print("38;2;{};{};{}", .{ rgb.r, rgb.g, rgb.b }),
|
||||
}
|
||||
}
|
||||
|
||||
// Background color
|
||||
if (reset_required and new.background != .Default or old != null and !old.?.background.eql(new.background)) {
|
||||
if (written_something) {
|
||||
try writer.writeAll(";");
|
||||
} else {
|
||||
written_something = true;
|
||||
}
|
||||
|
||||
switch (new.background) {
|
||||
.Default => try writer.writeAll("49"),
|
||||
.Black => try writer.writeAll("40"),
|
||||
.Red => try writer.writeAll("41"),
|
||||
.Green => try writer.writeAll("42"),
|
||||
.Yellow => try writer.writeAll("43"),
|
||||
.Blue => try writer.writeAll("44"),
|
||||
.Magenta => try writer.writeAll("45"),
|
||||
.Cyan => try writer.writeAll("46"),
|
||||
.White => try writer.writeAll("47"),
|
||||
.Fixed => |fixed| try writer.print("48;5;{}", .{fixed}),
|
||||
.Grey => |grey| try writer.print("48;2;{};{};{}", .{ grey, grey, grey }),
|
||||
.RGB => |rgb| try writer.print("48;2;{};{};{}", .{ rgb.r, rgb.g, rgb.b }),
|
||||
}
|
||||
}
|
||||
|
||||
// End the escape sequence
|
||||
try writer.writeAll("m");
|
||||
}
|
||||
|
||||
test "same style default, no update" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), Style{}, Style{});
|
||||
|
||||
const expected = "";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "same style non-default, no update" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
const sty = Style{
|
||||
.foreground = Color.Green,
|
||||
};
|
||||
try updateStyle(fixed_buf_stream.writer(), sty, sty);
|
||||
|
||||
const expected = "";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "reset to default, old null" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), Style{}, null);
|
||||
|
||||
const expected = "\x1B[0m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "reset to default, old non-null" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), Style{}, Style{
|
||||
.font_style = FontStyle.bold,
|
||||
});
|
||||
|
||||
const expected = "\x1B[0m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "bold style" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), Style{
|
||||
.font_style = FontStyle.bold,
|
||||
}, Style{});
|
||||
|
||||
const expected = "\x1B[1m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "add bold style" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), Style{
|
||||
.font_style = FontStyle{ .bold = true, .italic = true },
|
||||
}, Style{
|
||||
.font_style = FontStyle.italic,
|
||||
});
|
||||
|
||||
const expected = "\x1B[1m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "reset required font style" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), Style{
|
||||
.font_style = FontStyle.bold,
|
||||
}, Style{
|
||||
.font_style = FontStyle{ .bold = true, .underline = true },
|
||||
});
|
||||
|
||||
const expected = "\x1B[0m\x1B[1m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "reset required color style" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), Style{
|
||||
.foreground = Color.Red,
|
||||
}, null);
|
||||
|
||||
const expected = "\x1B[0m\x1B[31m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "no reset required color style" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), Style{
|
||||
.foreground = Color.Red,
|
||||
}, Style{});
|
||||
|
||||
const expected = "\x1B[31m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "no reset required add color style" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), Style{
|
||||
.foreground = Color.Red,
|
||||
.background = Color.Magenta,
|
||||
}, Style{
|
||||
.background = Color.Magenta,
|
||||
});
|
||||
|
||||
const expected = "\x1B[31m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
pub fn resetStyle(writer: anytype) !void {
|
||||
try writer.writeAll(reset);
|
||||
}
|
||||
|
||||
test "reset style" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
|
||||
try resetStyle(fixed_buf_stream.writer());
|
||||
|
||||
const expected = "\x1B[0m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "Grey foreground color" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
var new_style = Style{};
|
||||
new_style.foreground = Color{ .Grey = 1 };
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), new_style, Style{});
|
||||
|
||||
const expected = "\x1B[38;2;1;1;1m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
||||
|
||||
test "Grey background color" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fixed_buf_stream = fixedBufferStream(&buf);
|
||||
var new_style = Style{};
|
||||
new_style.background = Color{ .Grey = 1 };
|
||||
|
||||
try updateStyle(fixed_buf_stream.writer(), new_style, Style{});
|
||||
|
||||
const expected = "\x1B[48;2;1;1;1m";
|
||||
const actual = fixed_buf_stream.getWritten();
|
||||
|
||||
try testing.expectEqualSlices(u8, expected, actual);
|
||||
}
|
36
src/main.zig
Normal file
36
src/main.zig
Normal file
|
@ -0,0 +1,36 @@
|
|||
const std = @import("std");
|
||||
const zpinner = @import("zpinner.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
|
||||
|
||||
const stdout_file = std.io.getStdOut().writer();
|
||||
|
||||
const zpinneropts = zpinner.Options{
|
||||
.suffix = " - counting sheeps",
|
||||
.chars = zpinner.chars.Earth,
|
||||
.style = zpinner.Style{
|
||||
.foreground = .Green,
|
||||
.background = .Black,
|
||||
},
|
||||
};
|
||||
|
||||
var zpin = zpinner.New(stdout_file.any(), zpinneropts);
|
||||
try zpin.start();
|
||||
std.time.sleep(5 * std.time.ns_per_s);
|
||||
try zpin.stop();
|
||||
|
||||
zpin.set_suffix(" - counting crows");
|
||||
|
||||
std.debug.print("All my codebases are belong to you\n", .{});
|
||||
try zpin.start();
|
||||
std.time.sleep(5 * std.time.ns_per_s);
|
||||
try zpin.stop();
|
||||
}
|
||||
|
||||
test "simple test" {
|
||||
var list = std.ArrayList(i32).init(std.testing.allocator);
|
||||
defer list.deinit(); // try commenting this out and see if zig detects the memory leak!
|
||||
try list.append(42);
|
||||
try std.testing.expectEqual(@as(i32, 42), list.pop());
|
||||
}
|
209
src/parse_style.zig
Normal file
209
src/parse_style.zig
Normal file
|
@ -0,0 +1,209 @@
|
|||
const std = @import("std");
|
||||
const expect = std.testing.expect;
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
|
||||
const style = @import("style.zig");
|
||||
const Style = style.Style;
|
||||
const FontStyle = style.FontStyle;
|
||||
const Color = style.Color;
|
||||
|
||||
const ParseState = enum {
|
||||
parse_8,
|
||||
parse_fg_non_8,
|
||||
parse_fg_256,
|
||||
parse_fg_red,
|
||||
parse_fg_green,
|
||||
parse_fg_blue,
|
||||
parse_bg_non_8,
|
||||
parse_bg_256,
|
||||
parse_bg_red,
|
||||
parse_bg_green,
|
||||
parse_bg_blue,
|
||||
};
|
||||
|
||||
/// Parses an ANSI escape sequence into a Style. Returns null when the
|
||||
/// string does not represent a valid style description
|
||||
pub fn parseStyle(code: []const u8) ?Style {
|
||||
if (code.len == 0 or std.mem.eql(u8, code, "0") or std.mem.eql(u8, code, "00")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var font_style = FontStyle{};
|
||||
var foreground: Color = .Default;
|
||||
var background: Color = .Default;
|
||||
|
||||
var state = ParseState.parse_8;
|
||||
var red: u8 = 0;
|
||||
var green: u8 = 0;
|
||||
|
||||
var iter = std.mem.split(u8, code, ";");
|
||||
while (iter.next()) |str| {
|
||||
const part = std.fmt.parseInt(u8, str, 10) catch return null;
|
||||
|
||||
switch (state) {
|
||||
.parse_8 => {
|
||||
switch (part) {
|
||||
0 => font_style = FontStyle{},
|
||||
1 => font_style.bold = true,
|
||||
2 => font_style.dim = true,
|
||||
3 => font_style.italic = true,
|
||||
4 => font_style.underline = true,
|
||||
5 => font_style.slowblink = true,
|
||||
6 => font_style.rapidblink = true,
|
||||
7 => font_style.reverse = true,
|
||||
8 => font_style.hidden = true,
|
||||
9 => font_style.crossedout = true,
|
||||
20 => font_style.fraktur = true,
|
||||
30 => foreground = Color.Black,
|
||||
31 => foreground = Color.Red,
|
||||
32 => foreground = Color.Green,
|
||||
33 => foreground = Color.Yellow,
|
||||
34 => foreground = Color.Blue,
|
||||
35 => foreground = Color.Magenta,
|
||||
36 => foreground = Color.Cyan,
|
||||
37 => foreground = Color.White,
|
||||
38 => state = ParseState.parse_fg_non_8,
|
||||
39 => foreground = Color.Default,
|
||||
40 => background = Color.Black,
|
||||
41 => background = Color.Red,
|
||||
42 => background = Color.Green,
|
||||
43 => background = Color.Yellow,
|
||||
44 => background = Color.Blue,
|
||||
45 => background = Color.Magenta,
|
||||
46 => background = Color.Cyan,
|
||||
47 => background = Color.White,
|
||||
48 => state = ParseState.parse_bg_non_8,
|
||||
49 => background = Color.Default,
|
||||
53 => font_style.overline = true,
|
||||
else => {
|
||||
return null;
|
||||
},
|
||||
}
|
||||
},
|
||||
.parse_fg_non_8 => {
|
||||
switch (part) {
|
||||
5 => state = ParseState.parse_fg_256,
|
||||
2 => state = ParseState.parse_fg_red,
|
||||
else => {
|
||||
return null;
|
||||
},
|
||||
}
|
||||
},
|
||||
.parse_fg_256 => {
|
||||
foreground = Color{ .Fixed = part };
|
||||
state = ParseState.parse_8;
|
||||
},
|
||||
.parse_fg_red => {
|
||||
red = part;
|
||||
state = ParseState.parse_fg_green;
|
||||
},
|
||||
.parse_fg_green => {
|
||||
green = part;
|
||||
state = ParseState.parse_fg_blue;
|
||||
},
|
||||
.parse_fg_blue => {
|
||||
foreground = Color{
|
||||
.RGB = .{
|
||||
.r = red,
|
||||
.g = green,
|
||||
.b = part,
|
||||
},
|
||||
};
|
||||
state = ParseState.parse_8;
|
||||
},
|
||||
.parse_bg_non_8 => {
|
||||
switch (part) {
|
||||
5 => state = ParseState.parse_bg_256,
|
||||
2 => state = ParseState.parse_bg_red,
|
||||
else => {
|
||||
return null;
|
||||
},
|
||||
}
|
||||
},
|
||||
.parse_bg_256 => {
|
||||
background = Color{ .Fixed = part };
|
||||
state = ParseState.parse_8;
|
||||
},
|
||||
.parse_bg_red => {
|
||||
red = part;
|
||||
state = ParseState.parse_bg_green;
|
||||
},
|
||||
.parse_bg_green => {
|
||||
green = part;
|
||||
state = ParseState.parse_bg_blue;
|
||||
},
|
||||
.parse_bg_blue => {
|
||||
background = Color{
|
||||
.RGB = .{
|
||||
.r = red,
|
||||
.g = green,
|
||||
.b = part,
|
||||
},
|
||||
};
|
||||
state = ParseState.parse_8;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (state != ParseState.parse_8)
|
||||
return null;
|
||||
|
||||
return Style{
|
||||
.foreground = foreground,
|
||||
.background = background,
|
||||
.font_style = font_style,
|
||||
};
|
||||
}
|
||||
|
||||
test "parse empty style" {
|
||||
try expectEqual(@as(?Style, null), parseStyle(""));
|
||||
try expectEqual(@as(?Style, null), parseStyle("0"));
|
||||
try expectEqual(@as(?Style, null), parseStyle("00"));
|
||||
}
|
||||
|
||||
test "parse bold style" {
|
||||
const actual = parseStyle("01");
|
||||
const expected = Style{
|
||||
.font_style = FontStyle.bold,
|
||||
};
|
||||
|
||||
try expectEqual(@as(?Style, expected), actual);
|
||||
}
|
||||
|
||||
test "parse yellow style" {
|
||||
const actual = parseStyle("33");
|
||||
const expected = Style{
|
||||
.foreground = Color.Yellow,
|
||||
|
||||
.font_style = FontStyle{},
|
||||
};
|
||||
|
||||
try expectEqual(@as(?Style, expected), actual);
|
||||
}
|
||||
|
||||
test "parse some fixed color" {
|
||||
const actual = parseStyle("38;5;220;1");
|
||||
const expected = Style{
|
||||
.foreground = Color{ .Fixed = 220 },
|
||||
|
||||
.font_style = FontStyle.bold,
|
||||
};
|
||||
|
||||
try expectEqual(@as(?Style, expected), actual);
|
||||
}
|
||||
|
||||
test "parse some rgb color" {
|
||||
const actual = parseStyle("38;2;123;123;123;1");
|
||||
const expected = Style{
|
||||
.foreground = Color{ .RGB = .{ .r = 123, .g = 123, .b = 123 } },
|
||||
|
||||
.font_style = FontStyle.bold,
|
||||
};
|
||||
|
||||
try expectEqual(@as(?Style, expected), actual);
|
||||
}
|
||||
|
||||
test "parse wrong rgb color" {
|
||||
const actual = parseStyle("38;2;123");
|
||||
try expectEqual(@as(?Style, null), actual);
|
||||
}
|
10
src/root.zig
Normal file
10
src/root.zig
Normal file
|
@ -0,0 +1,10 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
export fn add(a: i32, b: i32) i32 {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
test "basic add functionality" {
|
||||
try testing.expect(add(3, 7) == 10);
|
||||
}
|
222
src/style.zig
Normal file
222
src/style.zig
Normal file
|
@ -0,0 +1,222 @@
|
|||
const std = @import("std");
|
||||
pub const format = @import("format.zig");
|
||||
pub const parse = @import("parse_style.zig");
|
||||
const meta = std.meta;
|
||||
const expect = std.testing.expect;
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
|
||||
pub const ColorRGB = struct {
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn eql(self: Self, other: Self) bool {
|
||||
return meta.eql(self, other);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Color = union(enum) {
|
||||
Default,
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Blue,
|
||||
Magenta,
|
||||
Cyan,
|
||||
White,
|
||||
Fixed: u8,
|
||||
Grey: u8,
|
||||
RGB: ColorRGB,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn eql(self: Self, other: Self) bool {
|
||||
return meta.eql(self, other);
|
||||
}
|
||||
};
|
||||
|
||||
pub const FontStyle = packed struct {
|
||||
bold: bool = false,
|
||||
dim: bool = false,
|
||||
italic: bool = false,
|
||||
underline: bool = false,
|
||||
slowblink: bool = false,
|
||||
rapidblink: bool = false,
|
||||
reverse: bool = false,
|
||||
hidden: bool = false,
|
||||
crossedout: bool = false,
|
||||
fraktur: bool = false,
|
||||
overline: bool = false,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub const bold = Self{
|
||||
.bold = true,
|
||||
};
|
||||
|
||||
pub const dim = Self{
|
||||
.dim = true,
|
||||
};
|
||||
|
||||
pub const italic = Self{
|
||||
.italic = true,
|
||||
};
|
||||
|
||||
pub const underline = Self{
|
||||
.underline = true,
|
||||
};
|
||||
|
||||
pub const slowblink = Self{
|
||||
.slowblink = true,
|
||||
};
|
||||
|
||||
pub const rapidblink = Self{
|
||||
.rapidblink = true,
|
||||
};
|
||||
|
||||
pub const reverse = Self{
|
||||
.reverse = true,
|
||||
};
|
||||
|
||||
pub const hidden = Self{
|
||||
.hidden = true,
|
||||
};
|
||||
|
||||
pub const crossedout = Self{
|
||||
.crossedout = true,
|
||||
};
|
||||
|
||||
pub const fraktur = Self{
|
||||
.fraktur = true,
|
||||
};
|
||||
|
||||
pub const overline = Self{
|
||||
.overline = true,
|
||||
};
|
||||
|
||||
pub fn toU11(self: Self) u11 {
|
||||
return @bitCast(self);
|
||||
}
|
||||
|
||||
pub fn fromU11(bits: u11) Self {
|
||||
return @bitCast(bits);
|
||||
}
|
||||
|
||||
/// Returns true iff this font style contains no attributes
|
||||
pub fn isDefault(self: Self) bool {
|
||||
return self.toU11() == 0;
|
||||
}
|
||||
|
||||
/// Returns true iff these font styles contain exactly the same
|
||||
/// attributes
|
||||
pub fn eql(self: Self, other: Self) bool {
|
||||
return self.toU11() == other.toU11();
|
||||
}
|
||||
|
||||
/// Returns true iff self is a subset of the attributes of
|
||||
/// other, i.e. all attributes of self are at least present in
|
||||
/// other as well
|
||||
pub fn subsetOf(self: Self, other: Self) bool {
|
||||
return self.toU11() & other.toU11() == self.toU11();
|
||||
}
|
||||
|
||||
/// Returns this font style with all attributes removed that are
|
||||
/// contained in other
|
||||
pub fn without(self: Self, other: Self) Self {
|
||||
return fromU11(self.toU11() & ~other.toU11());
|
||||
}
|
||||
};
|
||||
|
||||
test "FontStyle bits" {
|
||||
try expectEqual(@as(u11, 0), (FontStyle{}).toU11());
|
||||
try expectEqual(@as(u11, 1), (FontStyle.bold).toU11());
|
||||
try expectEqual(@as(u11, 1 << 2), (FontStyle.italic).toU11());
|
||||
try expectEqual(@as(u11, 1 << 2) | 1, (FontStyle{ .bold = true, .italic = true }).toU11());
|
||||
try expectEqual(FontStyle{}, FontStyle.fromU11((FontStyle{}).toU11()));
|
||||
try expectEqual(FontStyle.bold, FontStyle.fromU11((FontStyle.bold).toU11()));
|
||||
}
|
||||
|
||||
test "FontStyle subsetOf" {
|
||||
const default = FontStyle{};
|
||||
const bold = FontStyle.bold;
|
||||
const italic = FontStyle.italic;
|
||||
const bold_and_italic = FontStyle{ .bold = true, .italic = true };
|
||||
|
||||
try expect(default.subsetOf(default));
|
||||
try expect(default.subsetOf(bold));
|
||||
try expect(bold.subsetOf(bold));
|
||||
try expect(!bold.subsetOf(default));
|
||||
try expect(!bold.subsetOf(italic));
|
||||
try expect(default.subsetOf(bold_and_italic));
|
||||
try expect(bold.subsetOf(bold_and_italic));
|
||||
try expect(italic.subsetOf(bold_and_italic));
|
||||
try expect(bold_and_italic.subsetOf(bold_and_italic));
|
||||
try expect(!bold_and_italic.subsetOf(bold));
|
||||
try expect(!bold_and_italic.subsetOf(italic));
|
||||
try expect(!bold_and_italic.subsetOf(default));
|
||||
}
|
||||
|
||||
test "FontStyle without" {
|
||||
const default = FontStyle{};
|
||||
const bold = FontStyle.bold;
|
||||
const italic = FontStyle.italic;
|
||||
const bold_and_italic = FontStyle{ .bold = true, .italic = true };
|
||||
|
||||
try expectEqual(default, default.without(default));
|
||||
try expectEqual(bold, bold.without(default));
|
||||
try expectEqual(default, bold.without(bold));
|
||||
try expectEqual(bold, bold.without(italic));
|
||||
try expectEqual(bold, bold_and_italic.without(italic));
|
||||
try expectEqual(italic, bold_and_italic.without(bold));
|
||||
try expectEqual(default, bold_and_italic.without(bold_and_italic));
|
||||
}
|
||||
|
||||
pub const Style = struct {
|
||||
foreground: Color = .Default,
|
||||
background: Color = .Default,
|
||||
font_style: FontStyle = FontStyle{},
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Returns true iff this style equals the other style in
|
||||
/// foreground color, background color and font style
|
||||
pub fn eql(self: Self, other: Self) bool {
|
||||
if (!self.font_style.eql(other.font_style))
|
||||
return false;
|
||||
|
||||
if (!meta.eql(self.foreground, other.foreground))
|
||||
return false;
|
||||
|
||||
return meta.eql(self.background, other.background);
|
||||
}
|
||||
|
||||
/// Returns true iff this style equals the default set of styles
|
||||
pub fn isDefault(self: Self) bool {
|
||||
return eql(self, Self{});
|
||||
}
|
||||
|
||||
pub const parse = @import("parse_style.zig").parseStyle;
|
||||
};
|
||||
|
||||
test "style equality" {
|
||||
const a = Style{};
|
||||
const b = Style{
|
||||
.font_style = FontStyle.bold,
|
||||
};
|
||||
const c = Style{
|
||||
.foreground = Color.Red,
|
||||
};
|
||||
|
||||
try expect(a.isDefault());
|
||||
|
||||
try expect(a.eql(a));
|
||||
try expect(b.eql(b));
|
||||
try expect(c.eql(c));
|
||||
|
||||
try expect(!a.eql(b));
|
||||
try expect(!b.eql(a));
|
||||
try expect(!a.eql(c));
|
||||
}
|
120
src/zpinner.zig
Normal file
120
src/zpinner.zig
Normal file
|
@ -0,0 +1,120 @@
|
|||
pub const std = @import("std");
|
||||
pub const chars = @import("characters.zig");
|
||||
pub const Color = @import("ansi.zig").Color;
|
||||
pub const Style = @import("ansi.zig").Style;
|
||||
pub const cursor = @import("cursor.zig");
|
||||
pub const format = @import("format.zig");
|
||||
|
||||
/// DefaultOptions is setting the default values
|
||||
const DefaultOptions = struct {
|
||||
/// delay is the speed of the indicator
|
||||
delay: u64 = std.time.ns_per_s * 0.1,
|
||||
/// chars to loop through see characters.zig
|
||||
chars: []const u8 = chars.Snake,
|
||||
/// prefix is the text preppended to the indicator
|
||||
prefix: []const u8 = "",
|
||||
/// suffix is the text appended to the indicator
|
||||
suffix: []const u8 = "",
|
||||
/// style the indicator should have
|
||||
style: Style = .{
|
||||
.foreground = Color.Default,
|
||||
.background = Color.Default,
|
||||
},
|
||||
};
|
||||
|
||||
pub const Options = DefaultOptions;
|
||||
|
||||
pub fn New(writer: anytype, opts: Options) Zpinner {
|
||||
return Zpinner.init(writer, opts);
|
||||
}
|
||||
|
||||
const Zpinner = struct {
|
||||
writer: std.io.AnyWriter,
|
||||
mutex: std.Thread.Mutex,
|
||||
delay: u64,
|
||||
active: bool,
|
||||
hide_cursor: bool,
|
||||
options: DefaultOptions,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(writer: anytype, opts: Options) Zpinner {
|
||||
return .{
|
||||
.writer = writer,
|
||||
.mutex = .{},
|
||||
.delay = 1,
|
||||
.active = false,
|
||||
.hide_cursor = true,
|
||||
.options = opts,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn start(self: *Self) !void {
|
||||
self.mutex.lock();
|
||||
|
||||
if (self.active or !is_tty()) {
|
||||
self.mutex.unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.hide_cursor) {
|
||||
_ = try self.writer.print("\x1B[?25l", .{});
|
||||
}
|
||||
self.active = true;
|
||||
self.mutex.unlock();
|
||||
|
||||
const thread = try std.Thread.spawn(.{}, Self.run, .{self});
|
||||
thread.detach();
|
||||
}
|
||||
|
||||
pub fn set_suffix(self: *Self, suffix: []const u8) void {
|
||||
self.options.suffix = suffix;
|
||||
}
|
||||
|
||||
pub fn set_prefix(self: *Self, prefix: []const u8) void {
|
||||
self.options.prefix = prefix;
|
||||
}
|
||||
|
||||
pub fn active(self: *Self) bool {
|
||||
return self.active;
|
||||
}
|
||||
|
||||
fn is_tty() bool {
|
||||
return std.io.getStdOut().isTty();
|
||||
}
|
||||
|
||||
fn run(self: *Self) !void {
|
||||
try format.updateStyle(self.writer, self.options.style, null);
|
||||
while (true) {
|
||||
// const chars = "⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏";
|
||||
var splits = std.mem.split(u8, self.options.chars, " ");
|
||||
while (splits.next()) |character| {
|
||||
self.mutex.lock();
|
||||
if (!self.active) {
|
||||
self.mutex.unlock();
|
||||
return;
|
||||
}
|
||||
try self.writer.print("\r{s}{s}{s}", .{ self.options.prefix, character, self.options.suffix });
|
||||
self.mutex.unlock();
|
||||
std.time.sleep(self.options.delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn stop(self: *Self) !void {
|
||||
try format.resetStyle(self.writer);
|
||||
self.mutex.lock();
|
||||
|
||||
defer self.mutex.unlock();
|
||||
|
||||
if (self.active) {
|
||||
self.active = false;
|
||||
|
||||
if (self.hide_cursor) {
|
||||
try cursor.showCursor(self.writer);
|
||||
// try try self.writer.print("\x1B[?25h", .{});
|
||||
}
|
||||
|
||||
try self.writer.print("\r", .{});
|
||||
}
|
||||
}
|
||||
};
|
Loading…
Reference in a new issue