├── .gitattributes ├── .gitignore ├── README.md ├── build.zig └── src ├── ansi.zig ├── main.zig └── writer.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # zig magic 2 | /zig-cache 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zinput 2 | 3 | A Zig command-line input library! 4 | 5 | - [zinput](#zinput) 6 | - [Usage](#usage) 7 | 8 | ## Usage 9 | ```zig 10 | const zinput = @import("zinput"); 11 | 12 | const my_string = try zinput.askString(allocator, "I need a string!", 128); 13 | defer allocator.free(my_string); 14 | ``` 15 | 16 | Check out the test in `main.zig` for an example! 17 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const Builder = @import("std").build.Builder; 2 | 3 | pub fn build(b: *Builder) void { 4 | const mode = b.standardReleaseOptions(); 5 | const lib = b.addStaticLibrary("zinput", "src/main.zig"); 6 | lib.setBuildMode(mode); 7 | lib.install(); 8 | 9 | var main_tests = b.addTest("src/main.zig"); 10 | main_tests.setBuildMode(mode); 11 | 12 | const test_step = b.step("test", "Run library tests"); 13 | test_step.dependOn(&main_tests.step); 14 | } 15 | -------------------------------------------------------------------------------- /src/ansi.zig: -------------------------------------------------------------------------------- 1 | fn escape(comptime literal: []const u8) []const u8 { 2 | return "\x1b[" ++ literal; 3 | } 4 | 5 | // zig fmt: off 6 | pub const Black = "30"; 7 | pub const Red = "31"; 8 | pub const Green = "32"; 9 | pub const Yellow = "33"; 10 | pub const Blue = "34"; 11 | pub const Magenta = "35"; 12 | pub const Cyan = "36"; 13 | pub const LightGray = "37"; 14 | pub const Default = "39"; 15 | pub const DarkGray = "90"; 16 | pub const LightRed = "91"; 17 | pub const LightGreen = "92"; 18 | pub const LightYellow = "93"; 19 | pub const LightBlue = "94"; 20 | pub const LightMagenta = "95"; 21 | pub const LightCyan = "96"; 22 | pub const White = "97"; 23 | // zig fmt: on 24 | 25 | pub fn Reset() []const u8 { 26 | return escape("0m"); 27 | } 28 | 29 | pub fn Foreground(comptime color: []const u8) []const u8 { 30 | return escape(color ++ "m"); 31 | } 32 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const writer = @import("writer.zig"); 4 | const Fg = writer.Fg; 5 | const OutputWriter = writer.OutputWriter; 6 | 7 | /// Caller must free memory. 8 | pub fn askString(allocator: std.mem.Allocator, prompt: []const u8, max_size: usize) ![]u8 { 9 | const in = std.io.getStdIn().reader(); 10 | const out = OutputWriter.init(std.io.getStdOut()); 11 | 12 | try out.writeSeq(.{ Fg.Cyan, "? ", Fg.White, prompt }); 13 | 14 | const result = try in.readUntilDelimiterAlloc(allocator, '\n', max_size); 15 | return if (std.mem.endsWith(u8, result, "\r")) result[0..(result.len - 1)] else result; 16 | } 17 | 18 | /// Caller must free memory. Max size is recommended to be a high value, like 512. 19 | pub fn askDirPath(allocator: std.mem.Allocator, prompt: []const u8, max_size: usize) ![]u8 { 20 | const out = OutputWriter.init(std.io.getStdOut()); 21 | 22 | while (true) { 23 | const path = try askString(allocator, prompt, max_size); 24 | if (!std.fs.path.isAbsolute(path)) { 25 | try out.writeSeq(.{ Fg.Red, "Error: Invalid directory, please try again.\n\n" }); 26 | allocator.free(path); 27 | continue; 28 | } 29 | 30 | var dir = std.fs.cwd().openDir(path, std.fs.Dir.OpenDirOptions{}) catch { 31 | try out.writeSeq(.{ Fg.Red, "Error: Invalid directory, please try again.\n\n" }); 32 | allocator.free(path); 33 | continue; 34 | }; 35 | 36 | dir.close(); 37 | return path; 38 | } 39 | } 40 | 41 | pub fn askBool(prompt: []const u8) !bool { 42 | const in = std.io.getStdIn().reader(); 43 | const out = OutputWriter.init(std.io.getStdOut()); 44 | 45 | var buffer: [1]u8 = undefined; 46 | 47 | while (true) { 48 | try out.writeSeq(.{ Fg.Cyan, "? ", Fg.White, prompt, Fg.DarkGray, " (y/n) > " }); 49 | 50 | const read = in.read(&buffer) catch continue; 51 | try in.skipUntilDelimiterOrEof('\n'); 52 | 53 | if (read == 0) return error.EndOfStream; 54 | 55 | switch (buffer[0]) { 56 | 'y' => return true, 57 | 'n' => return false, 58 | else => continue, 59 | } 60 | } 61 | } 62 | 63 | pub fn askSelectOne(prompt: []const u8, comptime options: type) !options { 64 | const in = std.io.getStdIn().reader(); 65 | const out = OutputWriter.init(std.io.getStdOut()); 66 | 67 | try out.writeSeq(.{ Fg.Cyan, "? ", Fg.White, prompt, Fg.DarkGray, " (select one)", "\n\n" }); 68 | 69 | comptime var max_size: usize = 0; 70 | inline for (@typeInfo(options).Enum.fields) |option| { 71 | try out.writeSeq(.{ " - ", option.name, "\n" }); 72 | if (option.name.len > max_size) max_size = option.name.len; 73 | } 74 | 75 | while (true) { 76 | var buffer: [max_size + 1]u8 = undefined; 77 | 78 | try out.writeSeq(.{ Fg.DarkGray, "\n>", " " }); 79 | 80 | var result = (in.readUntilDelimiterOrEof(&buffer, '\n') catch { 81 | try in.skipUntilDelimiterOrEof('\n'); 82 | try out.writeSeq(.{ Fg.Red, "Error: Invalid option, please try again.\n" }); 83 | continue; 84 | }) orelse return error.EndOfStream; 85 | result = if (std.mem.endsWith(u8, result, "\r")) result[0..(result.len - 1)] else result; 86 | 87 | inline for (@typeInfo(options).Enum.fields) |option| 88 | if (std.ascii.eqlIgnoreCase(option.name, result)) 89 | return @intToEnum(options, option.value); 90 | // return option.value; 91 | 92 | try out.writeSeq(.{ Fg.Red, "Error: Invalid option, please try again.\n" }); 93 | } 94 | 95 | // return undefined; 96 | } 97 | 98 | test "basic input functionality" { 99 | std.debug.print("\n\n", .{}); 100 | 101 | std.debug.print("Welcome to the ZLS configuration wizard! (insert mage emoji here)\n", .{}); 102 | 103 | // const stdp = try askDirPath(testing.allocator, "What is your Zig lib path (path that contains the 'std' folder)?", 128); 104 | // const snippet = try askBool("Do you want to enable snippets?"); 105 | // const style = try askBool("Do you want to enable style warnings?"); 106 | const select = try askSelectOne("Which code editor do you use?", enum { VSCode, Sublime, Other }); 107 | 108 | // defer testing.allocator.free(select); 109 | 110 | if (select == .VSCode) {} 111 | 112 | std.debug.print("\n\n", .{}); 113 | } 114 | -------------------------------------------------------------------------------- /src/writer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ansi = @import("ansi.zig"); 3 | const Writer = std.fs.File.Writer; 4 | const builtin = @import("builtin"); 5 | 6 | const targeting_windows = (builtin.os.tag == .windows); 7 | const windows = std.os.windows; 8 | const wincon = struct { 9 | pub extern "kernel32" fn GetConsoleMode(h_console: windows.HANDLE, mode: *windows.DWORD) callconv(windows.WINAPI) windows.BOOL; 10 | pub extern "kernel32" fn GetConsoleScreenBufferInfo(h_console: windows.HANDLE, info: *windows.CONSOLE_SCREEN_BUFFER_INFO) callconv(windows.WINAPI) windows.BOOL; 11 | pub extern "kernel32" fn SetConsoleTextAttribute(h_console: windows.HANDLE, attrib: windows.DWORD) callconv(windows.WINAPI) windows.BOOL; 12 | }; 13 | 14 | pub const Fg = enum { 15 | Black, 16 | Red, 17 | Green, 18 | Yellow, 19 | Blue, 20 | Magenta, 21 | Cyan, 22 | LightGray, 23 | DarkGray, 24 | LightRed, 25 | LightGreen, 26 | LightYellow, 27 | LightBlue, 28 | LightMagenta, 29 | LightCyan, 30 | White, 31 | }; 32 | 33 | /// Ignores color specifications 34 | const PlainWriter = struct { 35 | writer: Writer, 36 | 37 | pub fn init(writer: Writer) PlainWriter { 38 | return PlainWriter{ 39 | .writer = writer, 40 | }; 41 | } 42 | 43 | pub fn writeSeq(self: *const PlainWriter, seq: anytype) !void { 44 | comptime var i: usize = 0; 45 | inline while (i < seq.len) : (i += 1) { 46 | const val = seq[i]; 47 | switch (@TypeOf(val)) { 48 | Fg => {}, 49 | else => { 50 | try self.writer.writeAll(val); 51 | }, 52 | } 53 | } 54 | } 55 | }; 56 | 57 | /// Handles color using ANSI vTerm sequences 58 | const AnsiWriter = struct { 59 | writer: Writer, 60 | 61 | pub fn init(writer: Writer) AnsiWriter { 62 | return AnsiWriter{ 63 | .writer = writer, 64 | }; 65 | } 66 | 67 | pub fn writeSeq(self: *const AnsiWriter, seq: anytype) !void { 68 | comptime var i: usize = 0; 69 | comptime var do_reset = false; 70 | inline while (i < seq.len) : (i += 1) { 71 | const val = seq[i]; 72 | switch (@TypeOf(val)) { 73 | Fg => { 74 | try self.writer.writeAll(AnsiWriter.fgSequence(val)); 75 | do_reset = true; 76 | }, 77 | else => { 78 | try self.writer.writeAll(val); 79 | }, 80 | } 81 | } 82 | if (do_reset) { 83 | try self.writer.writeAll(ansi.Reset()); 84 | } 85 | } 86 | 87 | fn fgSequence(fg: Fg) []const u8 { 88 | return switch (fg) { 89 | Fg.Black => ansi.Foreground(ansi.Black), 90 | Fg.Red => ansi.Foreground(ansi.Red), 91 | Fg.Green => ansi.Foreground(ansi.Green), 92 | Fg.Yellow => ansi.Foreground(ansi.Yellow), 93 | Fg.Blue => ansi.Foreground(ansi.Blue), 94 | Fg.Magenta => ansi.Foreground(ansi.Magenta), 95 | Fg.Cyan => ansi.Foreground(ansi.Cyan), 96 | Fg.LightGray => ansi.Foreground(ansi.LightGray), 97 | Fg.DarkGray => ansi.Foreground(ansi.DarkGray), 98 | Fg.LightRed => ansi.Foreground(ansi.LightRed), 99 | Fg.LightGreen => ansi.Foreground(ansi.LightGreen), 100 | Fg.LightYellow => ansi.Foreground(ansi.LightYellow), 101 | Fg.LightBlue => ansi.Foreground(ansi.LightBlue), 102 | Fg.LightMagenta => ansi.Foreground(ansi.LightMagenta), 103 | Fg.LightCyan => ansi.Foreground(ansi.LightCyan), 104 | Fg.White => ansi.Foreground(ansi.White), 105 | }; 106 | } 107 | }; 108 | 109 | /// Coloring text using Windows Console API 110 | const WinConWriter = struct { 111 | writer: Writer, 112 | orig_attribs: windows.DWORD, 113 | 114 | pub fn init(writer: Writer) WinConWriter { 115 | var tmp: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; 116 | _ = wincon.GetConsoleScreenBufferInfo(writer.context.handle, &tmp); 117 | 118 | return WinConWriter{ 119 | .writer = writer, 120 | .orig_attribs = tmp.wAttributes, 121 | }; 122 | } 123 | 124 | pub fn writeSeq(self: *const WinConWriter, seq: anytype) !void { 125 | const handle = self.writer.context.handle; 126 | 127 | comptime var i: usize = 0; 128 | comptime var do_reset = false; 129 | inline while (i < seq.len) : (i += 1) { 130 | const val = seq[i]; 131 | switch (@TypeOf(val)) { 132 | Fg => { 133 | const foreground_mask = @as(windows.WORD, 0b1111); 134 | const new_attrib = (self.orig_attribs & (~foreground_mask)) | WinConWriter.winConAttribValue(val); 135 | _ = wincon.SetConsoleTextAttribute(handle, new_attrib); 136 | do_reset = true; 137 | }, 138 | else => try self.writer.writeAll(val), 139 | } 140 | } 141 | if (do_reset) 142 | _ = wincon.SetConsoleTextAttribute(handle, self.orig_attribs); 143 | } 144 | 145 | fn winConAttribValue(fg: Fg) windows.DWORD { 146 | const blue = windows.FOREGROUND_BLUE; 147 | const green = windows.FOREGROUND_GREEN; 148 | const red = windows.FOREGROUND_RED; 149 | const bright = windows.FOREGROUND_INTENSITY; 150 | 151 | return switch (fg) { 152 | Fg.Black => 0, 153 | Fg.Red => red, 154 | Fg.Green => green, 155 | Fg.Yellow => green | red, 156 | Fg.Blue => blue, 157 | Fg.Magenta => red | blue, 158 | Fg.Cyan => green | blue, 159 | Fg.LightGray => red | green | blue, 160 | Fg.DarkGray => bright, 161 | Fg.LightRed => red | bright, 162 | Fg.LightGreen => green | bright, 163 | Fg.LightYellow => red | green | bright, 164 | Fg.LightBlue => blue | bright, 165 | Fg.LightMagenta => red | blue | bright, 166 | Fg.LightCyan => blue | green | bright, 167 | Fg.White => red | green | blue | bright, 168 | }; 169 | } 170 | }; 171 | 172 | const WriterImpl = if (targeting_windows) 173 | union(enum) { 174 | Plain: PlainWriter, 175 | Ansi: AnsiWriter, 176 | WinCon: WinConWriter, 177 | } 178 | else 179 | union(enum) { 180 | Plain: PlainWriter, 181 | Ansi: AnsiWriter, 182 | }; 183 | 184 | pub const OutputWriter = struct { 185 | impl: WriterImpl, 186 | 187 | pub fn writeSeq(self: *const OutputWriter, seq: anytype) !void { 188 | if (targeting_windows) { 189 | switch (self.impl) { 190 | WriterImpl.Plain => |w| try w.writeSeq(seq), 191 | WriterImpl.Ansi => |w| try w.writeSeq(seq), 192 | WriterImpl.WinCon => |w| try w.writeSeq(seq), 193 | } 194 | } else { 195 | switch (self.impl) { 196 | WriterImpl.Plain => |w| try w.writeSeq(seq), 197 | WriterImpl.Ansi => |w| try w.writeSeq(seq), 198 | } 199 | } 200 | } 201 | 202 | pub fn init(output: std.fs.File) OutputWriter { 203 | if (targeting_windows) { 204 | var mode: windows.DWORD = 0; 205 | if (wincon.GetConsoleMode(output.handle, &mode) != windows.FALSE) { 206 | const ENABLE_VIRTUAL_TERMINAL_PROCESSING: windows.DWORD = 0x0004; 207 | if (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) { 208 | return OutputWriter{ 209 | .impl = WriterImpl{ .Ansi = AnsiWriter.init(output.writer()) }, 210 | }; 211 | } 212 | } else { 213 | return OutputWriter{ 214 | .impl = WriterImpl{ .Plain = PlainWriter.init(output.writer()) }, 215 | }; 216 | } 217 | 218 | // Check if we run under ConEmu 219 | const wstr = std.unicode.utf8ToUtf16LeStringLiteral; 220 | if (std.os.getenvW(wstr("ConEmuANSI"))) |val| { 221 | if (std.mem.eql(u16, val, wstr("ON"))) { 222 | return OutputWriter{ 223 | .impl = WriterImpl{ .Ansi = AnsiWriter.init(output.writer()) }, 224 | }; 225 | } 226 | } 227 | 228 | return OutputWriter{ 229 | .impl = WriterImpl{ .WinCon = WinConWriter.init(output.writer()) }, 230 | }; 231 | } else { 232 | // TODO: Examine isatty() & env(TERM) to make a decision 233 | return OutputWriter{ 234 | .impl = WriterImpl{ .Ansi = AnsiWriter.init(output.writer()) }, 235 | }; 236 | } 237 | } 238 | }; 239 | --------------------------------------------------------------------------------