This commit is contained in:
2026-09-13 17:02:18 +12:00
commit 569a4ddfdc
12 changed files with 1587 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
ui.json
.zig-cache/
zig-out/

52
build.zig Normal file
View File

@@ -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);
}

12
build.zig.zon Normal file
View File

@@ -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",
},
}

37
example.json Normal file
View File

@@ -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"
}
]
}
]

59
readme.md Normal file
View File

@@ -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/).

BIN
res/example_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

169
src/ansi.zig Normal file
View File

@@ -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,
},
);
}

199
src/colour.zig Normal file
View File

@@ -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),
);
}
};

439
src/main.zig Normal file
View File

@@ -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);
}

18
src/root.zig Normal file
View File

@@ -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);
}

574
src/scan_codes.zig Normal file
View File

@@ -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> -> char
// <esc> <nochar> -> esc
// <esc> <esc> -> esc
// <esc> <char> -> Alt-keypress or keycode sequence
// <esc> '[' <nochar> -> Alt-[
// <esc> '[' (<modifier>) <char> -> keycode sequence, <modifier> is a decimal
// number and defaults to 1 (xterm)
// <esc> '[' 1 ; <modifier> <char> -> xterm
// <esc> '[' (<keycode>) (';'<modifier>) '~' -> keycode sequence, <keycode> and <modifier>
// 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',
};

25
ui.schema.json Normal file
View File

@@ -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"}
}
}
}
}
}
}