commit 569a4ddfdc04b6847302dc5255997c2aeaf4f8ab Author: sirlilpanda Date: Sun Sep 13 17:02:18 2026 +1200 init :) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a64e6bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +ui.json +.zig-cache/ +zig-out/ \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..26897a0 --- /dev/null +++ b/build.zig @@ -0,0 +1,52 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const mod = b.addModule("zig_server_documention", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + }); + + const exe = b.addExecutable(.{ + .name = "zig_server_documention", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "zig_server_documention", .module = mod }, + }, + }), + }); + + b.installArtifact(exe); + + const run_step = b.step("run", "Run the app"); + + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const mod_tests = b.addTest(.{ + .root_module = mod, + }); + + const run_mod_tests = b.addRunArtifact(mod_tests); + + const exe_tests = b.addTest(.{ + .root_module = exe.root_module, + }); + + const run_exe_tests = b.addRunArtifact(exe_tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); + test_step.dependOn(&run_exe_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..2eef688 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .zig_server_documention, + .version = "0.0.0", + .fingerprint = 0xa4a5c99d1a862ab7, // Changing this has security and trust implications. + .minimum_zig_version = "0.16.0", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example.json b/example.json new file mode 100644 index 0000000..0b03368 --- /dev/null +++ b/example.json @@ -0,0 +1,37 @@ +[ + { + "ascii_art": " ╔═══════╗\n ║ ╸╸╸╸ @║\n ║│╳╳╳╳│*║\n ║│╳╳╳╳│.║\n ╟┤╳╳╳╳├╌╢\n ║│╳╳╳╳│ ║\n ║│╳╳╳╳│ ║\n ╚╧════╧═╝", + "ip": "10.0.0.9", + "name": "truenas", + "services": [ + { + "name": "git", + "ports": [ + 3069 + ], + "description": "services for running my forge" + }, + { + "name": "lang tools", + "ports": [ + 8081 + ], + "description": "spell checker so i dont have to download the 300GB of the english language" + } + ] + }, + { + "ascii_art": " ╔══════╤══════╤══════╤══════╤══════╗\n ║╳╳╳╳╳@╏╳╳╳╳╳@╏╳╳╳╳╳@╏╳╳╳╳╳@╏╸=[]@*║\n ╚══════╧══════╧══════╧══════╧══════╝", + "ip": "10.0.0.10", + "name": "the big boy", + "services": [ + { + "name": "minecraft", + "ports": [ + 25565 + ], + "description": "26.2 minecraft server" + } + ] + } +] \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..f83e540 --- /dev/null +++ b/readme.md @@ -0,0 +1,59 @@ +# Server documentation tool, ill work out a fun name later + +This is fun small tool used to document your homelab its very simplistic and should be used more as a fun show case and not as real documentation. + +![example image](res/example_image.png) + +### how to use it + +first compile the project with zig 0.16.0, + +```bash +$ zig build +``` + +then its time to create your UI, create a json file called what ever you want and follow the schema provided ([`ui.schema.json`](ui.schema.json)) + + +you file should look something like this ([`example.json`](example.json)) +```json +[ + { + "ascii_art": " ╔═══════╗\n ║ ╸╸╸╸ @║\n ║│╳╳╳╳│*║\n ║│╳╳╳╳│.║\n ╟┤╳╳╳╳├╌╢\n ║│╳╳╳╳│ ║\n ║│╳╳╳╳│ ║\n ╚╧════╧═╝", + "ip": "10.0.0.9", + "name": "truenas", + "services": [ + { + "name": "git", + "ports": [ + 3069 + ], + "description": "services for running my forge" + }, + { + "name": "lang tools", + "ports": [ + 8081 + ], + "description": "spell checker so i dont have to download the 300GB of the english language" + } + ] + }, + { + "ascii_art": " ╔══════╤══════╤══════╤══════╤══════╗\n ║╳╳╳╳╳@╏╳╳╳╳╳@╏╳╳╳╳╳@╏╳╳╳╳╳@╏╸=[]@*║\n ╚══════╧══════╧══════╧══════╧══════╝", + "ip": "10.0.0.10", + "name": "the big boy", + "services": [ + { + "name": "minecraft", + "ports": [ + 25565 + ], + "description": "26.2 minecraft server" + } + ] + } +] +``` + +Finally to run pass the *relative* path of the json to the executable located in [`zig-out/bin/`](zig-out/bin/). \ No newline at end of file diff --git a/res/example_image.png b/res/example_image.png new file mode 100644 index 0000000..7a8d98c Binary files /dev/null and b/res/example_image.png differ diff --git a/src/ansi.zig b/src/ansi.zig new file mode 100644 index 0000000..b3c0d16 --- /dev/null +++ b/src/ansi.zig @@ -0,0 +1,169 @@ +const std = @import("std"); +const Colour = @import("colour.zig").Colour; + +pub const colour_set_string_fg_fmt = "\x1B[38;2;{d};{d};{d}m"; +pub const colour_set_string_bg_fmt = "\x1B[48;2;{d};{d};{d}m"; + +/// Enable terminal raw mode, return previous configuration. +pub fn enableRawMode() !std.os.linux.termios { + const orig_termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO); + var termios = orig_termios; + termios.lflag.ECHO = false; // don't echo input characters + termios.lflag.ICANON = false; // read input byte-by-byte instead of line-by-line + termios.lflag.ISIG = false; // disable Ctrl-C and Ctrl-Z signals + termios.iflag.IXON = false; // disable Ctrl-S and Ctrl-Q signals + termios.lflag.IEXTEN = false; // disable Ctrl-V + termios.iflag.ICRNL = false; // CTRL-M being read as CTRL-J + termios.iflag.BRKINT = false; // break conditions cause SIGINT signal + termios.iflag.INPCK = false; // disable parity checking (obsolete?) + termios.iflag.ISTRIP = false; // disable stripping of 8th bit + termios.cflag.CSIZE = .CS8; // set character size to 8 bits + termios.cc[@intFromEnum(std.os.linux.V.MIN)] = 0; // Return immediately when any bytes are available + termios.cc[@intFromEnum(std.os.linux.V.TIME)] = 1; // Wait up to 0.1 seconds for input + // termios.oflag.OPOST = false; // disable output processing + + try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, termios); + + return orig_termios; +} + +/// Disable terminal raw mode by restoring the saved configuration. +pub fn disableRawMode(termios: std.os.linux.termios) void { + // force termios back in to a useable state + var cpy = termios; + cpy.lflag.ICANON = true; + cpy.lflag.ECHO = true; + cpy.lflag.ISIG = true; + cpy.iflag.IXON = true; + cpy.lflag.IEXTEN = true; + cpy.iflag.ICRNL = true; + cpy.iflag.BRKINT = false; + cpy.iflag.INPCK = false; + cpy.iflag.ISTRIP = false; + cpy.cflag.CSIZE = .CS8; + cpy.oflag.OPOST = true; // disable output processing + cpy.cc[@intFromEnum(std.os.linux.V.MIN)] = 1; + cpy.cc[@intFromEnum(std.os.linux.V.TIME)] = 0; + std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, cpy) catch @panic("Disabling raw mode failed!"); +} + +pub fn eraseInLine(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[K"); +} + +pub fn clearScreen(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[2J"); +} + +pub fn showCursor(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[?25h"); +} + +pub fn hideCursor(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[?25l"); +} + +pub fn startBold(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[1m"); +} + +pub fn endBold(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[22m"); +} + +pub fn startDim(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[2m"); +} + +pub fn endDim(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[22m"); +} + +pub fn startItalic(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[3m"); +} + +pub fn endItalic(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[23m"); +} + +pub fn startUnderline(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[4m"); +} + +pub fn endUnderline(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[24m"); +} + +pub fn startBlinking(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[5m"); +} + +pub fn endBlinking(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[25m"); +} + +pub fn startInverse(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[7m"); +} + +pub fn endInverse(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[27m"); +} + +pub fn startHidden(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[8m"); +} + +pub fn endHidden(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[28m"); +} + +pub fn startStrikethrough(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[9m"); +} + +pub fn endStrikethrough(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[29m"); +} + +pub fn endMode(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[m"); +} + +pub fn returnToHome(writer: *std.Io.Writer) !void { + // try self.appendStr("\x1bOH"); + try placeCursorAt(writer, 0, 0); +} + +pub fn placeCursorAt(writer: *std.Io.Writer, x: usize, y: usize) !void { + try writer.print("\x1b[{};{}H", .{ y, x }); +} + +pub fn startColour(writer: *std.Io.Writer, foreground: Colour, background: Colour) !void { + try writer.print( + colour_set_string_fg_fmt ++ colour_set_string_bg_fmt, + .{ + foreground.r, foreground.b, foreground.g, + background.r, background.b, background.g, + }, + ); +} + +pub fn startFgColour(writer: *std.Io.Writer, colour: Colour) !void { + try writer.print( + colour_set_string_fg_fmt, + .{ + colour.r, colour.b, colour.g, + }, + ); +} + +pub fn startBgColour(writer: *std.Io.Writer, colour: Colour) !void { + try writer.print( + colour_set_string_bg_fmt, + .{ + colour.r, colour.b, colour.g, + }, + ); +} diff --git a/src/colour.zig b/src/colour.zig new file mode 100644 index 0000000..b0c99e2 --- /dev/null +++ b/src/colour.zig @@ -0,0 +1,199 @@ +const std = @import("std"); + +/// colour type +pub const Colour = struct { + const Self = @This(); + + r: u8, + g: u8, + b: u8, + + /// inits the struct with a custom colour + pub fn custom(r: u8, g: u8, b: u8) Self { + return Self{ .r = r, .g = g, .b = b }; + } + + /// inits the struct with a red colour + pub fn red() Self { + return Self{ .r = 255, .g = 0, .b = 0 }; + } + + /// inits the struct with a orange colour + pub fn orange() Self { + return Self{ .r = 255, .g = 128, .b = 0 }; + } + + /// inits the struct with a yellow colour + pub fn yellow() Self { + return Self{ .r = 255, .g = 255, .b = 0 }; + } + + /// inits the struct with a gold colour + pub fn gold() Self { + return Self{ .r = 255, .g = 200, .b = 0 }; + } + + /// inits the struct with a lightGreen colour + pub fn lightGreen() Self { + return Self{ .r = 128, .g = 255, .b = 0 }; + } + + /// inits the struct with a green colour + pub fn green() Self { + return Self{ .r = 0, .g = 255, .b = 0 }; + } + + /// inits the struct with a cyan colour + pub fn cyan() Self { + return Self{ .r = 0, .g = 255, .b = 128 }; + } + + /// inits the struct with a turquoise colour + pub fn turquoise() Self { + return Self{ .r = 0, .g = 255, .b = 190 }; + } + + /// inits the struct with a lightBlue colour + pub fn lightBlue() Self { + return Self{ .r = 0, .g = 128, .b = 255 }; + } + + /// inits the struct with a blue colour + pub fn blue() Self { + return Self{ .r = 0, .g = 0, .b = 255 }; + } + + /// inits the struct with a darkPurple colour + pub fn darkPurple() Self { + return Self{ .r = 128, .g = 0, .b = 255 }; + } + + /// inits the struct with a purple colour + pub fn purple() Self { + return Self{ .r = 170, .g = 0, .b = 255 }; + } + + /// inits the struct with a lightPurple colour + pub fn lightPurple() Self { + return Self{ .r = 196, .g = 78, .b = 255 }; + } + + /// inits the struct with a magenta colour + pub fn magenta() Self { + return Self{ .r = 255, .g = 0, .b = 255 }; + } + + /// inits the struct with a pink colour + pub fn pink() Self { + return Self{ .r = 255, .g = 0, .b = 128 }; + } + + /// inits the struct with a peach colour + pub fn peach() Self { + return Self{ .r = 255, .g = 158, .b = 158 }; + } + + /// inits the struct with a black colour + pub fn black() Self { + return Self{ .r = 0, .g = 0, .b = 0 }; + } + + /// inits the struct with a white colour + pub fn white() Self { + return Self{ .r = 255, .g = 255, .b = 255 }; + } + + /// inits the struct with a grey colour + pub fn grey() Self { + return Self{ .r = 128, .g = 128, .b = 128 }; + } + + /// inits the struct with a greyScale colour + pub fn greyScale(scale: u8) Self { + // 255 max + return Self{ .r = scale, .g = scale, .b = scale }; + } + + /// this is the background colour of the defualt windows terminal + pub fn windowsTerminalBackground() Self { + return greyScale(12); + } + + /// this is the colour of the defualt windows terminal front + pub fn windowsTerminalFont() Self { + return greyScale(204); + } + + /// point must be an unsigned number type + pub fn pointToColour(point: anytype) Colour { + const normed_point_hue: u16 = @as(u16, @intFromFloat((@as(f32, @floatFromInt(point)) / @as(f32, @floatFromInt(std.math.maxInt(@TypeOf(point))))) * 255 * 5)) + 255; + const normed_point_rgb: u8 = @intCast((normed_point_hue + 1) % 256); + + if (normed_point_hue <= 255 * 2) { //count up + return custom(255, normed_point_rgb, 0); + } + // 0, 255, 0, 255*3 + if (normed_point_hue <= 255 * 3) { // count down + return custom(255 - normed_point_rgb, 255, 0); + } + // 0, 255, 255, 255*4 + if (normed_point_hue <= 255 * 4) { // count up + return custom(0, 255, normed_point_rgb); + } + // 0, 0, 255, 255*5 + if (normed_point_hue <= 255 * 5) { // count down + return custom(0, 255 - normed_point_rgb, 255); + } + // 255, 0, 255, 255*6 + if (normed_point_hue <= 255 * 6) { // count up + return custom(normed_point_rgb, 0, 255); + } + + return Colour.white(); + } + + pub fn rangeToColour(start: usize, end: usize, point: usize) Colour { + const delta = end - start; + const shifted_point: usize = point - start; + // wrapping mul to insure that this will never overflow + const normed_point_pos: usize = (std.math.maxInt(usize) / delta) *% shifted_point; + return pointToColour(normed_point_pos); + } + + /// this will return a set colour based on the usize number + /// how ever at at point it will start to be random + pub fn usizeToColour(number: usize) Self { + return switch (number) { + 0 => red(), + 1 => orange(), + 2 => yellow(), + 3 => gold(), + 4 => lightGreen(), + 5 => green(), + 6 => cyan(), + 7 => turquoise(), + 8 => lightBlue(), + 9 => blue(), + 10 => darkPurple(), + 11 => purple(), + 12 => lightPurple(), + 13 => magenta(), + 14 => pink(), + 15 => peach(), + else => random(), // just make a random colour + }; + } + + /// returns a random colour + pub fn random() Self { + const static = struct { + var prng = std.rand.DefaultPrng.init(69420); + }; + const rand = static.prng.random(); + return custom( + rand.int(u8), + rand.int(u8), + rand.int(u8), + ); + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..954e61e --- /dev/null +++ b/src/main.zig @@ -0,0 +1,439 @@ +const std = @import("std"); +const Io = std.Io; +const ansi = @import("ansi.zig"); +const keys = @import("scan_codes.zig"); +const zig_server_documention = @import("zig_server_documention"); + +const Service = struct { + name: []const u8, + ports: []const u16, + description: []const u8, + + const none = Service{ + .name = "None", + .ports = &.{0}, + .description = "", + }; +}; + +const Server = struct { + const Self = @This(); + + ascii_art: []const u8, + ip: []const u8, + services: []const Service, + name: []const u8, +}; + +const UI = struct { + const Self = @This(); + const Lists = enum { + servers, + services, + }; + + writer: *std.Io.Writer, + reader: *std.Io.Reader, + + servers: []const Server, + current_server_index: usize = 0, + current_service_index: usize = 0, + + active_list: Lists = .servers, + + const text_box_width = 54; + const text_box_heading = " ┌─{s:-<41}─port:{d:-<5}─┐ "; + const text_box_footer = " └──────────────────────────────────────────────────────┘ "; + + const ui = + \\ + \\ ╔══════════════════════════════════════╦═══════════════════════════════════════════════════════════╗ + \\ ║ ║ name : {s: <51}║ + \\ ║ ║ ip : {s: <53}║ + \\ ║ ╟───────────────────────────────────────────────────────────╢ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ║ ║ ║ + \\ ╚══════════════════════════════════════╩═══════════════════════════════════════════════════════════╝ + \\ + ; + + // returns how many lines were written + pub fn drawTextAt(self: Self, offset_x: u16, offset_y: u16, wrap_length: u16, text: []const u8) !usize { + var itter = std.mem.splitScalar(u8, text, '\n'); + + var line_index: usize = 0; + while (itter.next()) |line| { + var utf8_view = try std.unicode.Utf8View.init(line); + var line_itter = utf8_view.iterator(); + + var i: usize = 0; + try ansi.placeCursorAt(self.writer, offset_x, offset_y + line_index); + while (line_itter.nextCodepointSlice()) |code_point| { + _ = try self.writer.write(code_point); + if (i >= wrap_length) { + try ansi.placeCursorAt(self.writer, offset_x, offset_y + line_index); + line_index += 1; + i = 0; + break; + } + i += 1; + } + line_index += 1; + } + try self.writer.flush(); + + return line_index; + } + + // might need to fix utf8 wrapping in this + fn drawServiceBox( + self: Self, + offset_x: u16, + offset_y: u16, + service: Service, + min_height: u16, + ) !usize { + var itter = std.mem.splitScalar(u8, service.description, '\n'); + + try ansi.placeCursorAt(self.writer, offset_x, offset_y); + try self.writer.print(text_box_heading, .{ service.name, service.ports[0] }); + + var line_index: usize = 1; + while (itter.next()) |line| { + const loops = @divFloor(line.len, text_box_width); + + for (0..loops) |i| { + try ansi.placeCursorAt(self.writer, offset_x, offset_y + line_index); + try self.writer.print(" │{s}│ ", .{line[i * text_box_width .. (i + 1) * text_box_width]}); + line_index += 1; + } + try ansi.placeCursorAt(self.writer, offset_x, offset_y + line_index); + try self.writer.print(" │{s: <54}│ ", .{line[loops * text_box_width ..]}); + line_index += 1; + } + + for (0..min_height -| line_index) |_| { + try ansi.placeCursorAt(self.writer, offset_x, offset_y + line_index); + try self.writer.print(" │{s: <54}│ ", .{" "}); + line_index += 1; + } + + try ansi.placeCursorAt(self.writer, offset_x, offset_y + line_index); + _ = try self.writer.write(text_box_footer); + try self.writer.flush(); + + return line_index + 1; + } + + fn getCurrentServer(self: Self) Server { + return self.servers[self.current_server_index]; + } + + pub fn render(self: Self) !void { + try self.writer.print(ui, .{ self.getCurrentServer().name, self.getCurrentServer().ip }); + + var y_offset: u16 = 3; + for (self.servers, 0..) |server, i| { + if (i == self.current_server_index) try ansi.startInverse(self.writer); + const lines_drawn = try self.drawTextAt(3, y_offset, 38, server.ascii_art); + y_offset += @intCast(lines_drawn); + try ansi.endMode(self.writer); + } + + // render services + y_offset = 6; + const x_offset = 42; + for (self.getCurrentServer().services, 0..) |service, i| { + if (i == self.current_service_index and self.active_list == .services) try ansi.startInverse(self.writer); + const lines_drawn = try self.drawServiceBox(x_offset, y_offset, service, 4); + y_offset += @intCast(lines_drawn); + try ansi.endMode(self.writer); + } + } + + fn addWrapping(value_to_wrap: *usize, wrap: usize, value_to_add: isize) void { + if (@as(isize, @intCast(value_to_wrap.*)) + value_to_add < 0) { + value_to_wrap.* = wrap - 1; + return; + } + + if (@as(isize, @intCast(value_to_wrap.*)) + value_to_add >= @as(isize, @intCast(wrap))) { + value_to_wrap.* = 0; + return; + } + + value_to_wrap.* = @intCast(@as(isize, @intCast(value_to_wrap.*)) + value_to_add); + } + + fn increamentActiveList(self: *Self, by: isize) void { + switch (self.active_list) { + .servers => addWrapping(&self.current_server_index, self.servers.len, by), + .services => addWrapping(&self.current_service_index, self.getCurrentServer().services.len, by), + } + } + + pub fn handleInput(self: *Self, input: enum { + up, + down, + left, + right, + }) void { + switch (input) { + .up => self.increamentActiveList(-1), + .down => self.increamentActiveList(1), + .left => { + switch (self.active_list) { + .servers => {}, + .services => self.active_list = .servers, + } + }, + .right => { + switch (self.active_list) { + .servers => self.active_list = .services, + .services => {}, + } + }, + } + } +}; + +pub fn main(init: std.process.Init) !void { + + // In order to do I/O operations need an `Io` instance. + const io = init.io; + + // Stdout is for the actual output of your application, for example if you + // are implementing gzip, then only the compressed bytes should be sent to + // stdout, not any debugging messages. + var stdout_buffer: [1024]u8 = undefined; + var stdin_buffer: [1024]u8 = undefined; + // var stderr_buffer: [1024]u8 = undefined; + + var stdin_file_reader: Io.File.Reader = .init(.stdin(), io, &stdin_buffer); + var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); + // var stderr_file_writer: Io.File.Writer = .init(.stderr(), io, &stderr_buffer); + const stdout_writer = &stdout_file_writer.interface; + const stdin_reader = &stdin_file_reader.interface; + + // var file_buffer: [1024]u8 = undefined; + // const cwd = std.Io.Dir.cwd(); + // const file = try cwd.createFile(io, "ui.json", .{}); + // var file_writer = file.writer(io, &file_buffer); + // var json = std.json.Stringify{ .writer = &file_writer.interface }; + // try json.write(ui.servers); + // try file_writer.flush(); + // file.close(io); + + var args = init.minimal.args.iterate(); + const cwd_path = args.next() orelse { + std.debug.print("please enter path to the json\n", .{}); + return error.no_file; + }; + _ = cwd_path; + const file_path = args.next() orelse { + std.debug.print("please enter path to the json\n", .{}); + return error.no_file; + }; + std.debug.print("path : {s}\n", .{file_path}); + + const cwd = std.Io.Dir.cwd(); + const file = try cwd.readFileAlloc(io, file_path, init.gpa, .unlimited); + + _ = try stdout_writer.write(file_path); + + const servers = try std.json.parseFromSlice([]Server, init.gpa, file, .{ .allocate = .alloc_always }); + defer init.gpa.free(file); + defer servers.deinit(); + + var ui = UI{ + .reader = stdin_reader, + .writer = stdout_writer, + // .servers = &.{ + // Server{ + // .ascii_art = + // \\ ╔═══════╗ + // \\ ║ ╸╸╸╸ @║ + // \\ ║│╳╳╳╳│*║ + // \\ ║│╳╳╳╳│.║ + // \\ ╟┤╳╳╳╳├╌╢ + // \\ ║│╳╳╳╳│ ║ + // \\ ║│╳╳╳╳│ ║ + // \\ ╚╧════╧═╝ + // , + // .ip = "10.0.0.9", + // .name = "the docs", + // .services = &.{ + // Service{ + // .name = "gitea", + // .ports = &.{ + // 3069, + // }, + // .description = "services for running my forge", + // }, + // Service{ + // .name = "lang tools", + // .ports = &.{ + // 8081, + // }, + // .description = "spell checker so i dont have to download the 300GB of the english language", + // }, + // Service{ + // .name = "miniflux", + // .ports = &.{ + // 3067, + // }, + // .description = "rss feed reader, \n-ratfactor\n-jordanhay\n- N-O-D-E\nlowtechmagazine", + // }, + // Service{ + // .name = "netalert", + // .ports = &.{ + // 1254, + // }, + // .description = "fing alternative", + // }, + // Service{ + // .name = "nginx reverse proxy manager", + // .ports = &.{ + // 81, + // 80, + // 443, + // }, + // .description = "handling ssl certs for the network", + // }, + // Service{ + // .name = "silver bullet", + // .ports = &.{ + // 3056, + // }, + // .description = "for multi device note taking", + // }, + // }, + // }, + // Server{ + // .ascii_art = + // \\ ╔══════╤══════╤══════╤══════╤══════╗ + // \\ ║╳╳╳╳╳@╏╳╳╳╳╳@╏╳╳╳╳╳@╏╳╳╳╳╳@╏╸=[]@*║ + // \\ ╚══════╧══════╧══════╧══════╧══════╝ + // , + // .ip = "10.0.0.29", + // .name = "the edmund fitzgerald", + // .services = &.{ + // Service{ + // .name = "minecraft survival", + // .ports = &.{ + // 25565, + // }, + // .description = "minecraft server 26.2, french village one", + // }, + // Service{ + // .name = "minecraft creative", + // .ports = &.{ + // 25566, + // }, + // .description = "minecraft server 26.2, for creative building", + // }, + // Service{ + // .name = "vintage story", + // .ports = &.{ + // 42420, + // }, + // .description = "vintage story server", + // }, + // }, + // }, + // Server{ + // .ascii_art = + // \\ ╔══════╤══════╤══════╤══════╤══════╗ + // \\ ║[###]▢╏[###]▢╏[###]▢╏[###]▢╏[###]▢║ + // \\ ║[###]▢╏[###]▢╏[###]▢╏[###]▢╏[###]▢║ + // \\ ║[###]▢╏[###]▢╏[###]▢╏[###]▢╏[###]▢║ + // \\ ╚══════╧══════╧══════╧══════╧══════╝ + // , + // .ip = "offline", + // .name = "Anemos", + // .services = &.{ + // .none, + // }, + // }, + // Server{ + // .ascii_art = + // \\ ╔══════╤══════╤══════╤══════╤══════╗ + // \\ ║[###]▢╏[###]▢╏[###]▢╏[###]▢╏[###]▢║ + // \\ ║[###]▢╏[###]▢╏[###]▢╏[###]▢╏[###]▢║ + // \\ ║[###]▢╏[###]▢╏[###]▢╏[###]▢╏[###]▢║ + // \\ ╚══════╧══════╧══════╧══════╧══════╝ + // , + // .ip = "offline", + // .name = "Artemis", + // .services = &.{ + // .none, + // }, + // }, + // }, + .servers = servers.value, + }; + + const term = try ansi.enableRawMode(); + try stdout_writer.flush(); + + while (true) { + const key = keys.parseKeycode(stdin_reader) catch continue orelse continue; + const exit_key_0 = comptime (keys.Key{ .escape = keys.ctrl('q') }).keyToU64(); + const exit_key_1 = comptime (keys.Key{ .escape = keys.ctrl('c') }).keyToU64(); + + const up_key = comptime (keys.Up.keyToU64()); + const down_key = comptime (keys.Down.keyToU64()); + const left_key = comptime (keys.Left.keyToU64()); + const right_key = comptime (keys.Right.keyToU64()); + + switch (key.keyToU64()) { + exit_key_0, exit_key_1 => break, + + up_key => ui.handleInput(.up), + down_key => ui.handleInput(.down), + left_key => ui.handleInput(.left), + right_key => ui.handleInput(.right), + else => {}, + } + try ansi.returnToHome(stdout_writer); + try ui.render(); + try stdout_writer.flush(); + } + try ansi.clearScreen(stdout_writer); + try ansi.placeCursorAt(stdout_writer, 0, 99); + try stdout_writer.flush(); + ansi.disableRawMode(term); +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..5a71250 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,18 @@ +//! By convention, root.zig is the root source file when making a package. +const std = @import("std"); +const Io = std.Io; + +/// This is a documentation comment to explain the `printAnotherMessage` function below. +/// +/// Accepting an `Io.Writer` instance is a handy way to write reusable code. +pub fn printAnotherMessage(writer: *Io.Writer) Io.Writer.Error!void { + try writer.print("Run `zig build test` to run the tests.\n", .{}); +} + +pub fn add(a: i32, b: i32) i32 { + return a + b; +} + +test "basic add functionality" { + try std.testing.expect(add(3, 7) == 10); +} diff --git a/src/scan_codes.zig b/src/scan_codes.zig new file mode 100644 index 0000000..15d461b --- /dev/null +++ b/src/scan_codes.zig @@ -0,0 +1,574 @@ +const std = @import("std"); + +const escape_char = 27; + +pub fn ctrl(comptime char: u8) u8 { + return switch (char) { + '@' => 0, + 'A', 'a' => 1, + 'B', 'b' => 2, + 'C', 'c' => 3, + 'D', 'd' => 4, + 'E', 'e' => 5, + 'F', 'f' => 6, + 'G', 'g' => 7, + 'H', 'h' => 8, + 'I', 'i' => 9, + 'J', 'j' => 10, + 'K', 'k' => 11, + 'L', 'l' => 12, + 'M', 'm' => 13, + 'N', 'n' => 14, + 'O', 'o' => 15, + 'P', 'p' => 16, + 'Q', 'q' => 17, + 'R', 'r' => 18, + 'S', 's' => 19, + 'T', 't' => 20, + 'U', 'u' => 21, + 'V', 'v' => 22, + 'W', 'w' => 23, + 'X', 'x' => 24, + 'Y', 'y' => 25, + 'Z', 'z' => 26, + '[' => 27, + '\\' => 28, + ']' => 29, + '^' => 30, + '_' => 31, + else => @compileError("that key does not have a ctrl value"), + }; +} + +pub fn isControlChar(char: u8) bool { + //yoniked https://github.com/torvalds/linux/blob/9ecfb2f7287a967b418ba69f10d45ead0d360593/tools/include/nolibc/ctype.h#L33 + return char < 0x20 or char == 0x7f; +} + +pub fn parseKeycode(reader: *std.Io.Reader) !?Key { + // if (self.poller.pollTimeout(100) catch false) { + // https://en.wikipedia.org/wiki/ANSI_escape_code#Terminal%20input%20sequences + // -> char + // -> esc + // -> esc + // -> Alt-keypress or keycode sequence + // '[' -> Alt-[ + // '[' () -> keycode sequence, is a decimal + // number and defaults to 1 (xterm) + // '[' 1 ; -> xterm + // '[' () (';') '~' -> keycode sequence, and + // are decimal numbers and default to 1 (vt) + + const char = try reader.takeByte(); + var k: Key = Key{}; + + if (isControlChar(char)) { + k.escape = char; + } else { + k.keycode = char; + } + + // not part of an espace sequence + if (char != '\x1b') { + return k; + } + + k.escape = char; + + const char_2 = try reader.takeByte(); + + // not part of an Alt-[ sequence + if (char_2 != '[' and char_2 != 'O') { + k.keycode = char_2; + return k; + } + + const char_3 = try reader.takeByte(); + k.keycode = char_3; + + // bt function keys the strange one + if (char_2 == 'O' and char_3 >= 'P' and char_3 <= 'S') { + k.keycode = char_2; + k.keycode_2 = char_3; + return k; + } + + // strange vt function keys we do the only convertion here + + // xterm check + if (char_3 >= 'A' and char_3 <= 'O' or char_3 >= 'T' and char_3 <= 'Z') { + return k; + } + + const char_4 = try reader.takeByte(); + + // modified xterm check + if (char_3 == '1' and char_4 == ';') { + const char_5 = try reader.takeByte(); + const char_6 = try reader.takeByte(); + k.modifier = char_5; + k.keycode = char_6; + return k; + } + + // xterm sequence + if (char_3 == 1 and char_4 >= 'P' and char_4 <= 'S') { + k.keycode_2 = char_4; + return k; + } + + if (char_4 == 0x1A) { + return k; + } + + // vt sequence with single keycode + if (char_4 == '~') { + return k; + } + + // vt sequence with multi keycode + if (char_3 >= '1' and char_3 <= '3' and char_4 != ';') { + k.keycode_2 = char_4; + const tilde = try reader.takeByte(); + if (tilde != '~') { + return null; + } + return k; + } + + if (char_4 != ';') { + k.modifier = char_3; + k.keycode = char_4; + return k; + } + + if (char_4 == ';') { + const char_5 = try reader.takeByte(); + _ = try reader.takeByte(); + k.modifier = char_5; + k.keycode = char_3; + return k; + } + + return null; +} + +pub const Key = struct { + const Self = @This(); + + escape: u8 = 0, + keycode: u8 = 0, + // this fucker is only needed for 2 chars sequences + keycode_2: u8 = 0, + // Shift 1 + // (Left) Alt 2 + // ctrl 4 + // meta 8 + modifier: u8 = 0, + + pub fn eq(a: Self, b: Self) bool { + return (a.escape == b.escape and + a.keycode == b.keycode and + a.modifier == b.modifier); + } + + // make it 64 to make things nice + // this allows for switch cases to be used + pub fn keyToU64(self: Self) u64 { + return (@as(u64, self.escape) << 24) + + (@as(u64, self.modifier) << 16) + + (@as(u64, self.keycode_2) << 8) + + @as(u64, self.keycode); + } + + pub fn isRenderable(self: Self) bool { + return switch (self.escape) { + 0 => true, + 9 => true, + else => false, + }; + } + + pub fn toChar(self: Self) u8 { + return switch (self.escape) { + 0 => self.keycode, + 9 => '\t', + else => 0, + }; + } + + pub fn format( + self: Self, + writer: *std.Io.Writer, + ) !void { + if (self.keycode_2 == 0) { + try writer.print( + "key(esc={}, keycode=\"{c}\", mod={})", + .{ + self.escape, + self.keycode, + self.modifier, + }, + ); + } else { + try writer.print( + "key(esc={}, keycode=|{c}|{}|, mod={}, id={})", + .{ + self.escape, + self.keycode, + self.keycode_2, + self.modifier, + self.keyToU64(), + }, + ); + } + } +}; + +// normal seqs + +pub const Enter: Key = Key{ + .escape = 13, +}; + +pub const Backspace: Key = Key{ + .escape = 127, +}; + +// vt sequences: +pub const Home_0: Key = Key{ + .escape = escape_char, + .keycode = '1', +}; + +pub const Insert: Key = Key{ + .escape = escape_char, + .keycode = '2', +}; + +pub const Delete: Key = Key{ + .escape = escape_char, + .keycode = '3', +}; + +pub const End_0: Key = Key{ + .escape = escape_char, + .keycode = '4', +}; + +pub const PgUp: Key = Key{ + .escape = escape_char, + .keycode = '5', +}; + +pub const PgDn: Key = Key{ + .escape = escape_char, + .keycode = '6', +}; + +pub const Home_1: Key = Key{ + .escape = escape_char, + .keycode = '7', +}; + +pub const End_1: Key = Key{ + .escape = escape_char, + .keycode = '8', +}; + +pub const F0: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '0', +}; + +pub const F1: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '1', +}; + +pub const F2: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '2', +}; + +pub const F3: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '3', +}; + +pub const F4: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '4', +}; + +pub const F5: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '5', +}; + +pub const F6: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '7', +}; + +pub const F7: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '8', +}; + +pub const F8: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '9', +}; + +pub const F9: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '0', +}; + +pub const F10: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '1', +}; + +pub const F11: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '3', +}; + +pub const F12: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '4', +}; + +pub const F13: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '5', +}; + +pub const F14: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '6', +}; + +pub const F15: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '8', +}; + +pub const F16: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '9', +}; + +pub const F17: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '1', +}; + +pub const F18: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '2', +}; + +pub const F19: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '3', +}; + +pub const F20: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '4', +}; + +pub const vt_9: Key = Key{ + .escape = escape_char, + .keycode = '9', +}; + +pub const vt_16: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '6', +}; + +pub const vt_22: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '2', +}; + +pub const vt_27: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '7', +}; + +pub const vt_30: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '0', +}; + +pub const vt_35: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '5', +}; + +// xterm sequences: + +pub const Up = Key{ + .escape = escape_char, + .keycode = 'A', +}; + +pub const Down = Key{ + .escape = escape_char, + .keycode = 'B', +}; + +pub const Right = Key{ + .escape = escape_char, + .keycode = 'C', +}; + +pub const Left = Key{ + .escape = escape_char, + .keycode = 'D', +}; + +pub const xterm_End = Key{ + .escape = escape_char, + .keycode = 'F', +}; + +pub const Keypad_5 = Key{ + .escape = escape_char, + .keycode = 'G', +}; + +pub const xterm_Home = Key{ + .escape = escape_char, + .keycode = 'H', +}; + +pub const xterm_F1 = Key{ + .escape = escape_char, + .modifier = 1, + .keycode = 'P', +}; + +pub const xterm_F2 = Key{ + .escape = escape_char, + .modifier = 1, + .keycode = 'Q', +}; + +pub const xterm_F3 = Key{ + .escape = escape_char, + .modifier = 1, + .keycode = 'R', +}; + +pub const xterm_F4 = Key{ + .escape = escape_char, + .modifier = 1, + .keycode = 'S', +}; + +pub const xterm_E = Key{ + .escape = escape_char, + .keycode = 'E', +}; + +pub const xterm_I = Key{ + .escape = escape_char, + .keycode = 'I', +}; + +pub const xterm_J = Key{ + .escape = escape_char, + .keycode = 'J', +}; + +pub const xterm_K = Key{ + .escape = escape_char, + .keycode = 'K', +}; + +pub const xterm_L = Key{ + .escape = escape_char, + .keycode = 'L', +}; + +pub const xterm_M = Key{ + .escape = escape_char, + .keycode = 'M', +}; + +pub const xterm_N = Key{ + .escape = escape_char, + .keycode = 'N', +}; + +pub const xterm_O = Key{ + .escape = escape_char, + .keycode = 'O', +}; + +pub const xterm_T = Key{ + .escape = escape_char, + .keycode = 'T', +}; + +pub const xterm_U = Key{ + .escape = escape_char, + .keycode = 'U', +}; + +pub const xterm_V = Key{ + .escape = escape_char, + .keycode = 'V', +}; + +pub const xterm_W = Key{ + .escape = escape_char, + .keycode = 'W', +}; + +pub const xterm_X = Key{ + .escape = escape_char, + .keycode = 'X', +}; + +pub const xterm_Y = Key{ + .escape = escape_char, + .keycode = 'Y', +}; + +pub const xterm_Z = Key{ + .escape = escape_char, + .keycode = 'Z', +}; diff --git a/ui.schema.json b/ui.schema.json new file mode 100644 index 0000000..0de6bc2 --- /dev/null +++ b/ui.schema.json @@ -0,0 +1,25 @@ +{ + "type": "array", + "items": { + "type" :"object", + "properties": { + "ascii_art" : {"type" : "string"}, + "ip" : {"type" : "string"}, + "name": {"type" : "string"}, + "services" : { + "type" : "array", + "items": { + "type" : "object", + "properties": { + "name" : {"type" : "string"}, + "ports" : { + "type" : "array", + "items": {"type" : "number"} + }, + "description" : {"type" : "string"} + } + } + } + } + } +} \ No newline at end of file