From 569a4ddfdc04b6847302dc5255997c2aeaf4f8ab Mon Sep 17 00:00:00 2001 From: sirlilpanda Date: Sun, 13 Sep 2026 17:02:18 +1200 Subject: [PATCH] init :) --- .gitignore | 3 + build.zig | 52 ++++ build.zig.zon | 12 + example.json | 37 +++ readme.md | 59 +++++ res/example_image.png | Bin 0 -> 30707 bytes src/ansi.zig | 169 +++++++++++++ src/colour.zig | 199 +++++++++++++++ src/main.zig | 439 ++++++++++++++++++++++++++++++++ src/root.zig | 18 ++ src/scan_codes.zig | 574 ++++++++++++++++++++++++++++++++++++++++++ ui.schema.json | 25 ++ 12 files changed, 1587 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 example.json create mode 100644 readme.md create mode 100644 res/example_image.png create mode 100644 src/ansi.zig create mode 100644 src/colour.zig create mode 100644 src/main.zig create mode 100644 src/root.zig create mode 100644 src/scan_codes.zig create mode 100644 ui.schema.json 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 0000000000000000000000000000000000000000..7a8d98cfb05a9f79613bacb76892ee51723c8e08 GIT binary patch literal 30707 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 literal 0 HcmV?d00001 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