diff --git a/src/characters.zig b/src/characters.zig deleted file mode 100644 index e29eeaf..0000000 --- a/src/characters.zig +++ /dev/null @@ -1,4 +0,0 @@ -pub const Moon = "🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘"; -pub const Snake = "⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏"; -pub const SnakeLoad = "⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷"; -pub const Earth = "🌍 🌎 🌏"; diff --git a/src/indicator.zig b/src/indicator.zig new file mode 100644 index 0000000..0b564a8 --- /dev/null +++ b/src/indicator.zig @@ -0,0 +1,22 @@ +pub const Moon = "🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘"; +pub const Snake = "⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏"; +pub const SnakeLoad = "⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷"; +pub const Earth = "🌍 🌎 🌏"; + +pub const Enum = enum { + Moon, + Snake, + SnakeLoad, + Earth, + + pub const EnumTable = [@typeInfo(Enum).Enum.fields.len][:0]const u8{ + Moon, + Snake, + SnakeLoad, + Earth, + }; + + pub fn str(self: Enum) [:0]const u8 { + return EnumTable[@intFromEnum(self)]; + } +}; diff --git a/src/main.zig b/src/main.zig index 6ac7c6c..4a281ba 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,31 +6,21 @@ pub fn main() !void { 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); + var zpin = zpinner.new(stdout_file.any(), .{}); + zpin.set_indicator(zpinner.indicator.Enum.Moon); + zpin.set_suffix(" calculating route to the moon"); try zpin.start(); std.time.sleep(5 * std.time.ns_per_s); try zpin.stop(); + try stdout_file.print("Go straight to /dev/null\n", .{}); - zpin.set_suffix(" - counting crows"); - - std.debug.print("All my codebases are belong to you\n", .{}); + zpin.set_suffix(" counting snakes"); + zpin.set_indicator(zpinner.indicator.Enum.Snake); try zpin.start(); std.time.sleep(5 * std.time.ns_per_s); + zpin.set_indicator(zpinner.indicator.Enum.Moon); + zpin.set_suffix(" calculating route back to earth"); + 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()); + try stdout_file.print("work done\n", .{}); } diff --git a/src/zpinner.zig b/src/zpinner.zig index b9086f2..253ce2e 100644 --- a/src/zpinner.zig +++ b/src/zpinner.zig @@ -1,16 +1,17 @@ pub const std = @import("std"); -pub const chars = @import("characters.zig"); +pub const indicator = @import("indicator.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"); +const builtin = @import("builtin"); /// 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, + indicator: indicator.Enum = indicator.Enum.Snake, /// prefix is the text preppended to the indicator prefix: []const u8 = "", /// suffix is the text appended to the indicator @@ -24,11 +25,23 @@ const DefaultOptions = struct { pub const Options = DefaultOptions; -pub fn New(writer: anytype, opts: Options) Zpinner { +fn IsWindowsTerminalOnWindows() bool { + const wt_session = std.posix.getenv("WT_SESSION") orelse ""; + if (wt_session.len > 0 and builtin.os.tag == .windows) { + return true; + } + + return false; +} + +/// Create new zpinner +/// Needs a writer().any() +pub fn new(writer: anytype, opts: Options) Zpinner { return Zpinner.init(writer, opts); } const Zpinner = struct { + thread: std.Thread, writer: std.io.AnyWriter, mutex: std.Thread.Mutex, delay: u64, @@ -38,7 +51,8 @@ const Zpinner = struct { const Self = @This(); - pub fn init(writer: anytype, opts: Options) Zpinner { + /// Initialize the zpinner + fn init(writer: anytype, opts: Options) Zpinner { return .{ .writer = writer, .mutex = .{}, @@ -46,75 +60,104 @@ const Zpinner = struct { .active = false, .hide_cursor = true, .options = opts, + .thread = undefined, }; } + /// Start zpinning pub fn start(self: *Self) !void { self.mutex.lock(); + try format.updateStyle(self.writer, self.options.style, null); if (self.active or !is_tty()) { self.mutex.unlock(); return; } - if (self.hide_cursor) { + if (self.hide_cursor and !IsWindowsTerminalOnWindows()) { _ = try self.writer.print("\x1B[?25l", .{}); } + self.active = true; self.mutex.unlock(); - const thread = try std.Thread.spawn(.{}, Self.run, .{self}); - thread.detach(); + self.thread = try std.Thread.spawn(.{}, Self.run, .{self}); } + /// Sets the appended text to the indicator pub fn set_suffix(self: *Self, suffix: []const u8) void { self.options.suffix = suffix; } + /// Sets the prepended text to the indicator pub fn set_prefix(self: *Self, prefix: []const u8) void { self.options.prefix = prefix; } + /// Activate the zpinner pub fn active(self: *Self) bool { return self.active; } + /// Set indicator + pub fn set_indicator(self: *Self, indicator_enum: indicator.Enum) void { + self.options.indicator = indicator_enum; + } + 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, " "); + var splits = std.mem.split(u8, self.options.indicator.str(), " "); while (splits.next()) |character| { - self.mutex.lock(); + if (!self.mutex.tryLock()) { + return; + } if (!self.active) { self.mutex.unlock(); return; } + + if (!IsWindowsTerminalOnWindows()) { + try self.erase(); + } + 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(); + fn erase(self: *Self) !void { + if (builtin.os.tag == .windows and !IsWindowsTerminalOnWindows()) { + const prefix_len = try std.unicode.utf8CountCodepoints(self.options.prefix); + const suffix_len = try std.unicode.utf8CountCodepoints(self.options.suffix); + try self.writer.print("\r", .{}); + try self.writer.writeByteNTimes(' ', prefix_len + suffix_len + 1); + try self.writer.print("\r", .{}); + return; + } + try self.writer.print("\r\x1B[K", .{}); + } + + /// Stops the zpinner + pub fn stop(self: *Self) !void { + self.mutex.lock(); defer self.mutex.unlock(); if (self.active) { self.active = false; - if (self.hide_cursor) { + if (self.hide_cursor and !IsWindowsTerminalOnWindows()) { try cursor.showCursor(self.writer); - // try try self.writer.print("\x1B[?25h", .{}); } - try self.writer.print("\r", .{}); + try self.erase(); + self.thread.join(); } } }; @@ -127,11 +170,11 @@ test "empty options struct contains default delay value" { try std.testing.expectEqual(@as(u64, defaultOptions.delay), @as(u64, expectedValue)); } test "empty options struct contains default snake charset" { - const expectedValue = chars.Snake; + const expectedValue = indicator.Snake; const opts = Options{}; - try std.testing.expectEqual(opts.chars, expectedValue); + try std.testing.expectEqual(opts.indicator.str(), expectedValue); } test "empty options struct contains default empty prefix" { const expectedValue = "";