repo: hackvr-turbo
action: commit
revision: 
path_from: 
revision_from: 5bf9a27176a9e6f3c1c7004909032e25229239c0:
path_to: 
revision_to: 
git.thebackupbox.net
hackvr-turbo
git clone git://git.thebackupbox.net/hackvr-turbo
commit 5bf9a27176a9e6f3c1c7004909032e25229239c0
Author: Felix (xq) Queißner 
Date:   Thu Jun 25 11:02:22 2020 +0200

    Improves parser, adds tests to parser, improves spec.

diff --git a/SPEC.md b/SPEC.md
index ab574c80377a0461f79273672d9a1c9dcd78cead..
index ..7d82681119c162d4a70e78206c8ed33504d6a907 100644
--- a/SPEC.md
+++ b/SPEC.md
@@ -3,45 +3,81 @@
 **Note:**
 This specification is inofficial and based on the development of HackVR Turbo (which is based on the original hackvr)

+## I/O
+HackVR communicates via `stdin`/`stdout`. Commands are passed on a line-by-line basis, each line is terminated by a LF character, optionally preceeded by a CR character.
+Each line must be encoded in valid UTF-8.
+
+## Commands
+
+Commands in a line are separated by one or more whitespace characters (SPACE, TAB). Commands are usually prefixed by a group selector `[group]`, only exception to that are `version` and `help`. 
+
+Everything after a `#` in a line is considered a comment and will be ignored by the command processor. The same is true for lines not containing any non-space characters.
+
+> TODO: group names can be globbed in some cases to operate on multiple groups
+>  some commands that take numbers as arguments can be made to behave relative by putting + before the number. makes negative relative a bit odd like:
+> ``` 
+> user move +-2 +-2 0
+> groupnam* command arguments
+> ```
+
+## Input Commands
+
+### `help`
+
+### `version`
+
+### `[groupspec] deleteallexcept grou*`
+
+### `[groupspec] deletegroup grou*`
+
+### `[groupspec] assimilate grou*`
+
+### `[groupspec] renamegroup group`
+
+### `[groupspec] status
+**deprecated**
+
+Just outputs a variable that is supposed to be loops per second.`
+
+### `[groupspec] dump
+Tries to let you output the various things that can be set.`
+
+### `[groupspec] quit
+Closes hackvr only if the id that is doing it is the same as yours.`
+
+### `[groupspec] set`
+
+### `[groupspec] physics`
+
+### `[groupspec] control grou*
+> Globbing this group could have fun effects
+
+### `[groupspec] addshape color N x1 y1 z1 ... xN yN zN`
+
+### `[groupspec] export grou*`
+
+### `[groupspec] ping any-string-without-spaces`
+
+### `[groupspec] scale x y z`
+
+### `[groupspec] rotate [+]x [+]y [+]z`
+
+### `[groupspec] periodic
+Flushes out locally-cached movement and rotation`
+
+### `[groupspec] flatten
+combines group attributes to the shapes.
+
+### `[groupspec] move [+]x [+]y [+]z`
+
+### `[groupspec] move forward|backward|up|down|left|right`
+
+## Output Commands
+
+### `name action targetgroup`
+
+when a group is clicked on using the name set from the command line. usually `$USER`

-hackvr help output:
-
-```
-commands that don't get prepended with groupname: help, version
-command format:
-group names can be globbed in some cases to operate on multiple groups
-some commands that take numbers as arguments can be made to behave relative
-by putting + before the number. makes negative relative a bit odd like:
-  user move +-2 +-2 0
-groupnam* command arguments
-commands:
-  deleteallexcept grou*
-_ deletegroup grou*
-  assimilate grou*
-  renamegroup group
-  status  # old. just outputs a variable that is supposed to be loops per second.
-  dump  # tries to let you output the various things that can be set.
-  quit  #closes hackvr only if the id that is doing it is the same as yours.
-  set
-  physics
-  control grou* [globbing this group could have fun effects]
-  addshape color N x1 y1 z1 ... xN yN zN
-  export grou*
-  ping any-string-without-spaces
-* scale x y z
-* rotate [+]x [+]y [+]z
-  periodic  # flushes out locally-cached movement and rotation
-  flatten  # combines group attributes to the shapes.
-* move [+]x [+]y [+]z
-* move forward|backward|up|down|left|right
-```
-
-hackvr also outputs
-
-```
-name action targetgroup
-when a group is clicked on using the name set from the command line. usually $USER
-```


 ## Changes
diff --git a/build.zig b/build.zig
index 852b2f32237a7a8c291d8a7c369f2437ca64b0a8..
index ..ec4ba1c2fd3fbb2b71fa392ba7e4004905767c50 100644
--- a/build.zig
+++ b/build.zig
@@ -3,6 +3,14 @@ const std = @import("std");
 const hackvr = std.build.Pkg{
     .name = "hackvr",
     .path = "lib/hackvr/lib.zig",
+    .dependencies = &[_]std.build.Pkg{
+        fixed_list,
+    },
+};
+
+const fixed_list = std.build.Pkg{
+    .name = "fixed-list",
+    .path = "lib/fixed_list.zig",
 };

 pub fn build(b: *std.build.Builder) void {
@@ -27,8 +35,11 @@ pub fn build(b: *std.build.Builder) void {

     exe.install();

+    const hackvr_tests = b.addTest("lib/hackvr/lib.zig");
+    hackvr_tests.addPackage(fixed_list);
+
     const test_step = b.step("test", "Runs all tests");
-    test_step.dependOn(&b.addTest("lib/hackvr/lib.zig").step);
+    test_step.dependOn(&hackvr_tests.step);

     const run_cmd = exe.run();
     run_cmd.step.dependOn(b.getInstallStep());
diff --git a/lib/fixed_list.zig b/lib/fixed_list.zig
new file mode 100644
index 0000000000000000000000000000000000000000..59fff865f365847cfdb9440442c39b9e25a3b197
--- /dev/null
+++ b/lib/fixed_list.zig
@@ -0,0 +1,41 @@
+const std = @import("std");
+
+pub fn FixedList(comptime T: type, comptime limit: usize) type {
+    return struct {
+        const Self = @This();
+
+        buffer: [limit]T,
+        count: usize,
+
+        pub fn init() Self {
+            return Self{
+                .buffer = undefined,
+                .count = 0,
+            };
+        }
+
+        pub fn append(self: *Self, value: T) !void {
+            if (self.count >= limit)
+                return error.OutOfMemory;
+            self.buffer[self.count] = value;
+            self.count += 1;
+        }
+
+        pub fn pop(self: *Self) ?T {
+            if (self.count > 0) {
+                self.count -= 1;
+                var value = self.buffer[self.count];
+                self.buffer[self.count] = undefined;
+                return value;
+            } else {
+                return null;
+            }
+        }
+
+        /// Return contents as a slice. Only valid while the list
+        /// doesn't change size.
+        pub fn span(self: *Self) []T {
+            return self.buffer[0..self.count];
+        }
+    };
+}
diff --git a/lib/hackvr/parser.zig b/lib/hackvr/parser.zig
index 188ae5811c4f6218a0435a1e9e3258d73556c45a..
index ..d9c5484e7f252cd4e06db908afede5d5e4fe0839 100644
--- a/lib/hackvr/parser.zig
+++ b/lib/hackvr/parser.zig
@@ -2,6 +2,8 @@ const std = @import("std");

 const hvr = @import("lib.zig");

+const FixedList = @import("fixed-list").FixedList;
+
 /// A push-event based parser for HackVR input.
 /// Parses hackvr text into serialized, preparsed commands.
 /// It's not interactive and can eat any amount of data.
@@ -20,31 +22,99 @@ pub const Parser = struct {
         };
     }

+    fn parseLine(line: []const u8) !?Event {
+        // max number of slices separated by a
+        var items = FixedList([]const u8, 512).init();
+
+        {
+            var current_start: usize = 0;
+            for (line) |c, i| {
+                if (std.ascii.isSpace(c)) {
+                    if (current_start < i) {
+                        try items.append(line[current_start..i]);
+                    }
+                    current_start = i + 1;
+                } else if (c == '#') {
+                    // rest is comment
+                    current_start = line.len;
+                    break;
+                }
+            }
+            if (current_start < line.len) {
+                try items.append(line[current_start..]);
+            }
+        }
+
+        switch (items.count) {
+            0 => return null,
+            1 => {
+                if (std.mem.eql(u8, "help", items.buffer[0])) {
+                    return Event{
+                        .event_type = .help,
+                    };
+                } else if (std.mem.eql(u8, "version", items.buffer[0])) {
+                    return Event{
+                        .event_type = .version,
+                    };
+                } else {
+                    return error.UnknownCommand;
+                }
+            },
+            else => {
+                return Event{
+                    .event_type = .not_implemented_yet,
+                };
+            },
+        }
+    }
+
     /// Pushes data into the parser.
     pub fn push(self: *Self, source: []const u8) !PushResult {
         var offset: usize = 0;

         while (offset < source.len) {
-            defer offset += 1;
             if (source[offset] == '\n') {
                 var line = self.line_buffer[0..self.line_offset];
+                self.line_offset = 0;
+                offset += 1;
+
+                if (line.len > 0 and line[line.len - 1] == '\r') {
+                    // strip off CR
+                    line = line[0 .. line.len - 1];
+                }
+
                 if (!std.unicode.utf8ValidateSlice(line))
                     return error.InvalidEncoding;

-                std.log.debug(.HackVR, "Emit event for line '{}'\n", .{line});
-
-                return PushResult{
-                    .event = .{
-                        .rest = source[0..offset],
-                        .event = Event{},
-                    },
-                };
+                const rest = source[offset..];
+
+                if (line.len > 0) {
+                    const event = parseLine(line) catch |err| switch (err) {
+                        error.UnknownCommand => return PushResult{
+                            .parse_error = .{
+                                .rest = rest,
+                                .error_type = .unknown_command,
+                            },
+                        },
+                        else => return err,
+                    };
+
+                    if (event) |ev| {
+                        return PushResult{
+                            .event = .{
+                                .rest = rest,
+                                .event = ev,
+                            },
+                        };
+                    }
+                }
+            } else {
+                if (self.line_offset > self.line_buffer.len)
+                    return error.OutOfMemory;
+                self.line_buffer[self.line_offset] = source[offset];
+                self.line_offset += 1;
+                offset += 1;
             }
-
-            if (self.line_offset > self.line_buffer.len)
-                return error.OutOfMemory;
-            self.line_buffer[self.line_offset] = source[offset];
-            self.line_offset += 1;
         }

         return PushResult{
@@ -73,6 +143,7 @@ pub const PushResult = union(enum) {
         const Error = enum {
             syntax_error,
             invalid_format,
+            unknown_command,
         };

         /// The portion of the input data that was not parsed
@@ -85,7 +156,16 @@ pub const PushResult = union(enum) {
     },
 };

-pub const Event = struct {};
+pub const Event = struct {
+    const Type = enum {
+        help,
+        version,
+
+        not_implemented_yet,
+    };
+
+    event_type: Type,
+};

 test "parser: invalid encoding" {
     var parser = Parser.init();
@@ -96,3 +176,48 @@ test "parser: invalid encoding" {
     };
     unreachable;
 }
+
+test "parser: empty" {
+    var parser = Parser.init();
+
+    const result = try parser.push("");
+    std.testing.expect(result == .needs_data);
+}
+
+test "parser: whitespace lines" {
+    var parser = Parser.init();
+
+    const result = try parser.push("\n   \n\n  \n \n \n\n\n    \n");
+    std.testing.expect(result == .needs_data);
+}
+
+test "parser: ParseResult.rest/error" {
+    var parser = Parser.init();
+
+    const result = try parser.push("a\nb");
+    std.testing.expect(result == .parse_error);
+    std.testing.expectEqualStrings("b", result.parse_error.rest);
+    std.testing.expect(result.parse_error.error_type == .unknown_command);
+}
+
+test "parser: ParseResult.rest/event" {
+    var parser = Parser.init();
+
+    const result = try parser.push("help\nb");
+    std.testing.expect(result == .event);
+    std.testing.expectEqualStrings("b", result.event.rest);
+}
+
+test "parser: parse line" {
+    _ = try Parser.parseLine("  a bb cccc ");
+}
+
+test "parser: cmd help" {
+    const result = (try Parser.parseLine("help")) orelse return error.ExpectedEvent;
+    std.testing.expect(result.event_type == .help);
+}
+
+test "parser: cmd version" {
+    const result = (try Parser.parseLine("version")) orelse return error.ExpectedEvent;
+    std.testing.expect(result.event_type == .version);
+}

-----END OF PAGE-----