diff --git a/tools/sorcerer/README.md b/tools/sorcerer/README.md index 19cc35f33..b4ca2f0aa 100644 --- a/tools/sorcerer/README.md +++ b/tools/sorcerer/README.md @@ -1 +1,127 @@ # Sorcerer + +Sorcerer is a suite of tools for visualizing and generating MicroZig register definitions. It +consists of two components: + +- **Sorcerer (GUI)**: A graphical application for browsing and editing register definitions +- **sorcerer-cli**: A command-line tool for generating register code + +Both tools work with MicroZig's register definition files (SVD, ATDF, Embassy formats) and use +[regz](../regz/) to generate type-safe Zig code. + +## Building + +From the `tools/sorcerer/` directory: + +```bash +# Build both tools +zig build +``` + +## Sorcerer GUI + +The GUI provides an interactive interface for: +- Browsing all MicroZig register definitions +- Viewing generated Zig code with syntax highlighting +- Opening custom SVD/ATDF files +- Searching chips, boards, and targets +- Viewing patch files that modify register definitions +- Statistics overview showing chip counts, formats, and patch coverage + +### Running + +```bash +zig build run +# or +./zig-out/bin/sorcerer +``` + +### Dependencies + +The GUI requires additional dependencies: +- DVUI (SDL3-based UI framework) +- tree-sitter-zig (syntax highlighting) +- serial (serial port communication) + +## `sorcerer-cli` + +A lightweight CLI alternative that generates register definitions without GUI dependencies. + +### Usage + +``` +sorcerer-cli [options] + +Commands: + list List all available targets + generate Generate register definitions for a chip + +Options for 'list': + --port Filter by port name (e.g., rp2xxx, ch32v) + --json Output in JSON format + +Options for 'generate': + -o, --output Output directory (default: ./zig-out) + +General options: + -h, --help Show help +``` + +### Examples + +```bash +# List all available chips +./zig-out/bin/sorcerer-cli list + +# Output: +# CHIP PORT BOARD +# ------------------------ ------------------------ -------------------------------- +# RP2040 raspberrypi/rp2xxx Raspberry Pi Pico +# RP2350 raspberrypi/rp2xxx Raspberry Pi Pico 2 +# STM32F103C8 stmicro/stm32 - +# CH32V003 wch/ch32v - +# ... + +# Filter by port +./zig-out/bin/sorcerer-cli list --port rp2xxx + +# JSON output (for scripting) +./zig-out/bin/sorcerer-cli list --json | jq '.[] | select(.port | contains("rp2xxx"))' + +# Generate register definitions for a chip +./zig-out/bin/sorcerer-cli generate RP2040 -o ./my-regs/ + +# This generates: +# ./my-regs/RP2040.zig +# ./my-regs/types.zig +# ./my-regs/types/ +``` + +### Running via build system + +```bash +# Run CLI with arguments +zig build run-cli -- list --port ch32v +zig build run-cli -- generate CH32V003 -o /tmp/regs +``` + +## Architecture + +Both tools share the same underlying data: + +1. **Build time**: `build.zig` collects all register definitions from MicroZig ports +2. **Schema generation**: Schemas are embedded as Zig compile-time constants in a generated + `register_schemas.zig` file +3. **Both GUI and CLI**: Import the same `schemas` module - no runtime file loading or JSON parsing + needed + +## Register Definition Formats + +Sorcerer supports the following formats via regz: + +| Format | Extension | Description | +|--------|-----------|-------------| +| SVD | `.svd` | ARM CMSIS System View Description | +| ATDF | `.atdf` | Microchip ATDF (AVR/SAM devices) | +| Embassy | - | Embassy HAL register definitions | +| TargetDB | - | TI TargetDB format | diff --git a/tools/sorcerer/build.zig b/tools/sorcerer/build.zig index 765c561a9..3dfeb05ce 100644 --- a/tools/sorcerer/build.zig +++ b/tools/sorcerer/build.zig @@ -14,14 +14,9 @@ pub fn build(b: *std.Build) void { const mb = MicroBuild.init(b, mz_dep) orelse return; const register_schemas = get_register_schemas(b, mb) catch @panic("OOM"); const write_files = b.addWriteFiles(); - const register_schema = write_files.add("register_schemas.json", std.json.Stringify.valueAlloc(b.allocator, register_schemas, .{}) catch @panic("OOM")); - const register_schema_install = b.addInstallFile(register_schema, "data/register_schemas.json"); - b.getInstallStep().dependOn(®ister_schema_install.step); - const dvui_dep = b.dependency("dvui", .{ - .target = target, - .optimize = optimize, - }); + // Generate Zig file with embedded schemas (used by both CLI and GUI) + const register_schema_zig = write_files.add("register_schemas.zig", generate_zig_schema_literal(b.allocator, register_schemas) catch @panic("OOM")); const regz_dep = mz_dep.builder.dependency("tools/regz", .{ .target = target, @@ -32,13 +27,63 @@ pub fn build(b: *std.Build) void { .optimize = .ReleaseSafe, }); + const regz_mod = regz_dep.module("regz"); + + // Shared module for RegisterSchemaUsage (used by both schemas_mod and cli_mod) + const register_schema_usage_mod = b.createModule(.{ + .root_source_file = b.path("src/RegisterSchemaUsage.zig"), + }); + + // Create schemas module from generated Zig file + const schemas_mod = b.createModule(.{ + .root_source_file = register_schema_zig, + .imports = &.{ + .{ .name = "RegisterSchemaUsage", .module = register_schema_usage_mod }, + }, + }); + + // ───────────────────────────────────────────────────────────────────────── + // CLI executable + // ───────────────────────────────────────────────────────────────────────── + const cli_mod = b.createModule(.{ + .root_source_file = b.path("src/cli.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "regz", .module = regz_mod }, + .{ .name = "schemas", .module = schemas_mod }, + .{ .name = "RegisterSchemaUsage", .module = register_schema_usage_mod }, + }, + }); + + const cli_exe = b.addExecutable(.{ + .name = "sorcerer-cli", + .root_module = cli_mod, + }); + b.installArtifact(cli_exe); + + const run_cli_cmd = b.addRunArtifact(cli_exe); + run_cli_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cli_cmd.addArgs(args); + } + const run_cli_step = b.step("run-cli", "Run the CLI tool"); + run_cli_step.dependOn(&run_cli_cmd.step); + + // ───────────────────────────────────────────────────────────────────────── + // GUI executable + // ───────────────────────────────────────────────────────────────────────── + const dvui_dep = b.dependency("dvui", .{ + .target = target, + .optimize = optimize, + }); + const serial_dep = b.dependency("serial", .{ .target = target, .optimize = optimize, }); const dvui_mod = dvui_dep.module("dvui_sdl3"); - const regz_mod = regz_dep.module("regz"); const serial_mod = serial_dep.module("serial"); const exe_mod = b.createModule(.{ @@ -58,6 +103,14 @@ pub fn build(b: *std.Build) void { .name = "serial", .module = serial_mod, }, + .{ + .name = "schemas", + .module = schemas_mod, + }, + .{ + .name = "RegisterSchemaUsage", + .module = register_schema_usage_mod, + }, }, }); @@ -94,7 +147,7 @@ pub fn build(b: *std.Build) void { run_cmd.addArgs(args); } - const run_step = b.step("run", "Run the app"); + const run_step = b.step("run", "Run the GUI app"); run_step.dependOn(&run_cmd.step); const exe_unit_tests = b.addTest(.{ @@ -320,11 +373,18 @@ fn get_register_schemas(b: *std.Build, mb: *MicroBuild) ![]const RegisterSchemaU const patch_files = try convert_patch_files(b, t.chip.patch_files); if (chips.getEntry(lazy_path)) |entry| { - try entry.value_ptr.append(b.allocator, .{ - .name = t.chip.name, - .target_name = twp.path, - .patch_files = patch_files, - }); + // Check if this chip name already exists (deduplicate by chip name) + const chip_exists = for (entry.value_ptr.items) |existing_chip| { + if (std.mem.eql(u8, existing_chip.name, t.chip.name)) break true; + } else false; + + if (!chip_exists) { + try entry.value_ptr.append(b.allocator, .{ + .name = t.chip.name, + .target_name = twp.path, + .patch_files = patch_files, + }); + } } else { var chip_list: std.ArrayList(RegisterSchemaUsage.Chip) = .{}; try chip_list.append(b.allocator, .{ @@ -372,7 +432,9 @@ fn get_port_name(path: []const u8) []const u8 { var i: u32 = 0; var slash_count: u32 = 0; while (i < path.len) : (i += 1) { - if (path[path.len - i - 1] == '/') { + const c = path[path.len - i - 1]; + // Handle both forward and backslashes for cross-platform compatibility + if (c == '/' or c == '\\') { switch (slash_count) { 0 => slash_count += 1, 1 => { @@ -386,3 +448,85 @@ fn get_port_name(path: []const u8) []const u8 { unreachable; } + +/// Normalize a path to use forward slashes (for cross-platform compatibility in generated Zig code). +/// Windows backslashes would be interpreted as escape sequences in Zig string literals. +fn normalize_path(allocator: std.mem.Allocator, path: []const u8) ![]const u8 { + const result = try allocator.alloc(u8, path.len); + for (path, 0..) |c, i| { + result[i] = if (c == '\\') '/' else c; + } + return result; +} + +/// Generate a Zig source file containing the register schemas as compile-time constants. +fn generate_zig_schema_literal(allocator: std.mem.Allocator, schemas: []const RegisterSchemaUsage) ![]const u8 { + var buf: std.ArrayList(u8) = .{}; + const writer = buf.writer(allocator); + + try writer.writeAll( + \\// Auto-generated file - do not edit manually. + \\// Generated by tools/sorcerer/build.zig + \\ + \\const RegisterSchemaUsage = @import("RegisterSchemaUsage"); + \\ + \\pub const schemas: []const RegisterSchemaUsage = &.{ + \\ + ); + + for (schemas) |schema| { + try writer.writeAll(" .{\n"); + + // Format + try writer.print(" .format = .{s},\n", .{@tagName(schema.format)}); + + // Chips + try writer.writeAll(" .chips = &.{"); + for (schema.chips, 0..) |chip, i| { + if (i > 0) try writer.writeAll(", "); + try writer.print(".{{ .name = \"{s}\" }}", .{chip.name}); + } + try writer.writeAll("},\n"); + + // Boards + try writer.writeAll(" .boards = &.{"); + for (schema.boards, 0..) |board, i| { + if (i > 0) try writer.writeAll(", "); + try writer.print(".{{ .name = \"{s}\" }}", .{board.name}); + } + try writer.writeAll("},\n"); + + // Location + try writer.writeAll(" .location = "); + switch (schema.location) { + .src_path => |src| { + const sub_path = try normalize_path(allocator, src.sub_path); + const build_root = try normalize_path(allocator, src.build_root); + try writer.writeAll(".{ .src_path = .{\n"); + try writer.print(" .port_name = \"{s}\",\n", .{src.port_name}); + try writer.print(" .sub_path = \"{s}\",\n", .{sub_path}); + try writer.print(" .build_root = \"{s}\",\n", .{build_root}); + try writer.writeAll(" } },\n"); + }, + .dependency => |dep| { + const sub_path = try normalize_path(allocator, dep.sub_path); + const build_root = try normalize_path(allocator, dep.build_root); + try writer.writeAll(".{ .dependency = .{\n"); + try writer.print(" .sub_path = \"{s}\",\n", .{sub_path}); + try writer.print(" .build_root = \"{s}\",\n", .{build_root}); + try writer.print(" .dep_name = \"{s}\",\n", .{dep.dep_name}); + try writer.print(" .port_name = \"{s}\",\n", .{dep.port_name}); + try writer.writeAll(" } },\n"); + }, + } + + try writer.writeAll(" },\n"); + } + + try writer.writeAll( + \\}; + \\ + ); + + return buf.toOwnedSlice(allocator); +} diff --git a/tools/sorcerer/src/RegzWindow.zig b/tools/sorcerer/src/RegzWindow.zig index 2b9b6d48a..0e6046e23 100644 --- a/tools/sorcerer/src/RegzWindow.zig +++ b/tools/sorcerer/src/RegzWindow.zig @@ -42,7 +42,7 @@ const Allocator = std.mem.Allocator; const regz = @import("regz"); const VirtualFilesystem = regz.VirtualFilesystem; -const RegisterSchemaUsage = @import("RegisterSchemaUsage.zig"); +const RegisterSchemaUsage = @import("RegisterSchemaUsage"); const dvui = @import("dvui"); diff --git a/tools/sorcerer/src/cli.zig b/tools/sorcerer/src/cli.zig new file mode 100644 index 000000000..2047a5879 --- /dev/null +++ b/tools/sorcerer/src/cli.zig @@ -0,0 +1,427 @@ +//! sorcerer-cli: Command-line interface for MicroZig register definitions +//! +//! A lightweight CLI tool that provides access to MicroZig register definitions without GUI +//! dependencies. Generates Zig register code from SVD/ATDF files. +//! +//! Usage: +//! sorcerer-cli list [--port ] [--json] +//! sorcerer-cli generate [-o ] +//! +const std = @import("std"); +const regz = @import("regz"); +const schemas = @import("schemas"); +const RegisterSchemaUsage = @import("RegisterSchemaUsage"); + +const Allocator = std.mem.Allocator; + +const StdoutWriter = struct { + buf: [4096]u8 = undefined, + file_writer: ?std.fs.File.Writer = null, + + fn writer(self: *StdoutWriter) *std.Io.Writer { + if (self.file_writer == null) { + self.file_writer = std.fs.File.stdout().writer(&self.buf); + } + return &self.file_writer.?.interface; + } +}; + +const StderrWriter = struct { + buf: [4096]u8 = undefined, + file_writer: ?std.fs.File.Writer = null, + + fn writer(self: *StderrWriter) *std.Io.Writer { + if (self.file_writer == null) { + self.file_writer = std.fs.File.stderr().writer(&self.buf); + } + return &self.file_writer.?.interface; + } +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + run(allocator) catch |err| { + switch (err) { + error.Explained => std.process.exit(1), + else => return err, + } + }; +} + +fn run(allocator: Allocator) !void { + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + if (args.len < 2) { + try print_usage(); + return error.Explained; + } + + const command = args[1]; + + if (std.mem.eql(u8, command, "list")) { + try run_list(allocator, args[2..]); + } else if (std.mem.eql(u8, command, "generate")) { + try run_generate(allocator, args[2..]); + } else if (std.mem.eql(u8, command, "-h") or std.mem.eql(u8, command, "--help")) { + try print_usage(); + } else { + var stderr_writer: StderrWriter = .{}; + const stderr = stderr_writer.writer(); + try stderr.print("Unknown command: {s}\n\n", .{command}); + try stderr.flush(); + try print_usage(); + return error.Explained; + } +} + +fn print_usage() !void { + var stdout_writer: StdoutWriter = .{}; + const stdout = stdout_writer.writer(); + try stdout.writeAll( + \\sorcerer-cli - MicroZig Register Definition Tool + \\ + \\Usage: + \\ sorcerer-cli [options] + \\ + \\Commands: + \\ list List all available targets + \\ generate Generate register definitions for a chip + \\ + \\Options for 'list': + \\ --port Filter by port name (e.g., rp2xxx, ch32v) + \\ --json Output in JSON format + \\ + \\Options for 'generate': + \\ -o, --output Output directory (default: ./zig-out) + \\ + \\General options: + \\ -h, --help Show this help + \\ + \\Examples: + \\ sorcerer-cli list + \\ sorcerer-cli list --port rp2xxx + \\ sorcerer-cli list --json + \\ sorcerer-cli generate RP2040 -o ./my-regs/ + \\ + ); + try stdout.flush(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// List command +// ───────────────────────────────────────────────────────────────────────────── + +fn run_list(allocator: Allocator, args: []const []const u8) !void { + var port_filter: ?[]const u8 = null; + var json_output = false; + + var i: usize = 0; + while (i < args.len) : (i += 1) { + const arg = args[i]; + if (std.mem.eql(u8, arg, "--port")) { + i += 1; + if (i >= args.len) { + var stderr_writer: StderrWriter = .{}; + const stderr = stderr_writer.writer(); + try stderr.writeAll("Error: --port requires a value\n"); + try stderr.flush(); + return error.Explained; + } + port_filter = args[i]; + } else if (std.mem.eql(u8, arg, "--json")) { + json_output = true; + } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { + try print_usage(); + return; + } else { + var stderr_writer: StderrWriter = .{}; + const stderr = stderr_writer.writer(); + try stderr.print("Unknown option: {s}\n", .{arg}); + try stderr.flush(); + return error.Explained; + } + } + + if (json_output) { + try print_list_json(allocator, port_filter); + } else { + try print_list_table(port_filter); + } +} + +fn print_list_table(port_filter: ?[]const u8) !void { + var stdout_writer: StdoutWriter = .{}; + const stdout = stdout_writer.writer(); + + // Print header + stdout.print("{s:<24} {s:<24} {s}\n", .{ "CHIP", "PORT", "BOARD" }) catch |err| return handle_write_error(err); + stdout.print("{s:-<24} {s:-<24} {s:-<32}\n", .{ "", "", "" }) catch |err| return handle_write_error(err); + + // Print entries + for (schemas.schemas) |schema| { + const port_name = get_port_name(schema.location); + + // Apply filter + if (port_filter) |filter| { + if (std.mem.indexOf(u8, port_name, filter) == null) { + continue; + } + } + + for (schema.chips) |chip| { + const board_name = if (schema.boards.len > 0) schema.boards[0].name else "-"; + stdout.print("{s:<24} {s:<24} {s}\n", .{ chip.name, port_name, board_name }) catch |err| return handle_write_error(err); + + // Print additional boards for same chip + if (schema.boards.len > 1) { + for (schema.boards[1..]) |board| { + stdout.print("{s:<24} {s:<24} {s}\n", .{ "", "", board.name }) catch |err| return handle_write_error(err); + } + } + } + } + stdout.flush() catch |err| return handle_write_error(err); +} + +/// Handle write errors - exit silently on BrokenPipe so that we can e.g. pipe to `more`. +fn handle_write_error(err: anyerror) error{Explained} { + return switch (err) { + error.BrokenPipe => { + // Pipe closed by reader (e.g., `head`). Exit silently. + std.process.exit(0); + }, + else => error.Explained, + }; +} + +fn print_list_json(allocator: Allocator, port_filter: ?[]const u8) !void { + var entries: std.ArrayList(JsonEntry) = .{}; + defer entries.deinit(allocator); + + for (schemas.schemas) |schema| { + const port_name = get_port_name(schema.location); + + // Apply filter + if (port_filter) |filter| { + if (std.mem.indexOf(u8, port_name, filter) == null) { + continue; + } + } + + for (schema.chips) |chip| { + try entries.append(allocator, .{ + .chip = chip.name, + .port = port_name, + .format = @tagName(schema.format), + .boards = schema.boards, + }); + } + } + + // Serialize to JSON string and print + const json_str = try std.json.Stringify.valueAlloc(allocator, entries.items, .{ .whitespace = .indent_2 }); + defer allocator.free(json_str); + + var stdout_writer: StdoutWriter = .{}; + const stdout = stdout_writer.writer(); + stdout.writeAll(json_str) catch |err| return handle_write_error(err); + stdout.writeByte('\n') catch |err| return handle_write_error(err); + stdout.flush() catch |err| return handle_write_error(err); +} + +const JsonEntry = struct { + chip: []const u8, + port: []const u8, + format: []const u8, + boards: []const RegisterSchemaUsage.Board, +}; + +fn get_port_name(location: RegisterSchemaUsage.Location) []const u8 { + return switch (location) { + .src_path => |src| src.port_name, + .dependency => |dep| dep.port_name, + }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Generate command +// ───────────────────────────────────────────────────────────────────────────── + +fn run_generate(allocator: Allocator, args: []const []const u8) !void { + var chip_name: ?[]const u8 = null; + var output_path: []const u8 = "./zig-out"; + + var i: usize = 0; + while (i < args.len) : (i += 1) { + const arg = args[i]; + if (std.mem.eql(u8, arg, "-o") or std.mem.eql(u8, arg, "--output")) { + i += 1; + if (i >= args.len) { + var stderr_writer: StderrWriter = .{}; + const stderr = stderr_writer.writer(); + try stderr.writeAll("Error: --output requires a value\n"); + try stderr.flush(); + return error.Explained; + } + output_path = args[i]; + } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { + try print_usage(); + return; + } else if (!std.mem.startsWith(u8, arg, "-")) { + chip_name = arg; + } else { + var stderr_writer: StderrWriter = .{}; + const stderr = stderr_writer.writer(); + try stderr.print("Unknown option: {s}\n", .{arg}); + try stderr.flush(); + return error.Explained; + } + } + + const chip = chip_name orelse { + var stderr_writer: StderrWriter = .{}; + const stderr = stderr_writer.writer(); + try stderr.writeAll("Error: chip name is required\n"); + try stderr.writeAll("Usage: sorcerer-cli generate [-o ]\n"); + try stderr.flush(); + return error.Explained; + }; + + // Find matching schema + const schema = find_schema(chip) orelse { + var stderr_writer: StderrWriter = .{}; + const stderr = stderr_writer.writer(); + try stderr.print("Error: chip '{s}' not found\n", .{chip}); + try stderr.writeAll("Use 'sorcerer-cli list' to see available chips\n"); + try stderr.flush(); + return error.Explained; + }; + + try generate_code(allocator, schema, chip, output_path); +} + +fn find_schema(chip_name: []const u8) ?RegisterSchemaUsage { + for (schemas.schemas) |schema| { + for (schema.chips) |chip| { + if (std.mem.eql(u8, chip.name, chip_name)) { + return schema; + } + } + } + return null; +} + +fn generate_code( + allocator: Allocator, + schema: RegisterSchemaUsage, + chip_name: []const u8, + output_path: []const u8, +) !void { + var stderr_writer: StderrWriter = .{}; + const stderr = stderr_writer.writer(); + var stdout_writer: StdoutWriter = .{}; + const stdout = stdout_writer.writer(); + + // Get full path to register definition file + const input_path = try get_full_path(allocator, schema.location); + defer allocator.free(input_path); + + try stdout.print("Generating register definitions for {s}...\n", .{chip_name}); + try stdout.print(" Input: {s}\n", .{input_path}); + try stdout.print(" Output: {s}/\n", .{output_path}); + try stdout.flush(); + + // Map format + const format: regz.Database.Format = switch (schema.format) { + .svd => .svd, + .atdf => .atdf, + .embassy => .embassy, + .targetdb => .targetdb, + }; + + // Create database from register definition file + var db = regz.Database.create_from_path(allocator, format, input_path, chip_name) catch |err| { + try stderr.print("Error loading register definition: {}\n", .{err}); + try stderr.flush(); + return error.Explained; + }; + defer db.destroy(); + + // Generate to virtual filesystem first + var vfs = regz.VirtualFilesystem.init(allocator); + defer vfs.deinit(); + + db.to_zig(vfs.dir(), .{}) catch |err| { + try stderr.print("Error generating Zig code: {}\n", .{err}); + try stderr.flush(); + return error.Explained; + }; + + // Write virtual filesystem contents to actual directory + var output_dir = std.fs.cwd().makeOpenPath(output_path, .{}) catch |err| { + try stderr.print("Error creating output directory: {}\n", .{err}); + try stderr.flush(); + return error.Explained; + }; + defer output_dir.close(); + + const files_written = try write_vfs_to_dir(allocator, &vfs, output_dir, .root, ""); + + try stdout.print("Generated {d} file(s)\n", .{files_written}); + try stdout.flush(); +} + +fn get_full_path(allocator: Allocator, location: RegisterSchemaUsage.Location) ![]const u8 { + return switch (location) { + .src_path => |src| try std.fmt.allocPrint(allocator, "{s}/{s}", .{ src.build_root, src.sub_path }), + .dependency => |dep| try std.fmt.allocPrint(allocator, "{s}/{s}", .{ dep.build_root, dep.sub_path }), + }; +} + +fn write_vfs_to_dir( + allocator: Allocator, + vfs: *regz.VirtualFilesystem, + output_dir: std.fs.Dir, + parent_id: regz.VirtualFilesystem.ID, + parent_path: []const u8, +) !usize { + var files_written: usize = 0; + + const children = try vfs.get_children(allocator, parent_id); + defer allocator.free(children); + + for (children) |child| { + const name = vfs.get_name(child.id); + const full_path = if (parent_path.len > 0) + try std.fmt.allocPrint(allocator, "{s}/{s}", .{ parent_path, name }) + else + try allocator.dupe(u8, name); + defer allocator.free(full_path); + + switch (child.kind) { + .file => { + const content = vfs.get_content(child.id); + + // Create subdirectory if needed + if (std.fs.path.dirname(full_path)) |dirname| { + try output_dir.makePath(dirname); + } + + const file = try output_dir.createFile(full_path, .{}); + defer file.close(); + try file.writeAll(content); + + files_written += 1; + }, + .directory => { + files_written += try write_vfs_to_dir(allocator, vfs, output_dir, child.id, full_path); + }, + } + } + + return files_written; +} diff --git a/tools/sorcerer/src/main.zig b/tools/sorcerer/src/main.zig index b429e5e97..6b72c8303 100644 --- a/tools/sorcerer/src/main.zig +++ b/tools/sorcerer/src/main.zig @@ -3,7 +3,8 @@ const builtin = @import("builtin"); const dvui = @import("dvui"); const serial = @import("serial"); const regz = @import("regz"); -const RegisterSchemaUsage = @import("RegisterSchemaUsage.zig"); +const schemas = @import("schemas"); +const RegisterSchemaUsage = @import("RegisterSchemaUsage"); const RegzWindow = @import("RegzWindow.zig"); const SrceryTheme = @import("SrceryTheme.zig"); @@ -45,8 +46,6 @@ var state: State = .{}; const State = struct { orig_content_scale: f32 = 1.0, - register_schema_path: []const u8 = "./zig-out/data/register_schemas.json", - register_schema_usages: ?std.json.Parsed([]const RegisterSchemaUsage) = null, regz_windows: std.StringArrayHashMapUnmanaged(*RegzWindow) = .{}, show_from_microzig_window: bool = false, show_stats_window: bool = true, @@ -68,27 +67,13 @@ const State = struct { // - runs between win.begin()/win.end() pub fn AppInit(win: *dvui.Window) !void { state.orig_content_scale = win.content_scale; - //try dvui.addFont("NOTO", @embedFile("../src/fonts/NotoSansKR-Regular.ttf"), null); // Use Srcery color scheme win.theme = SrceryTheme.theme; std.log.info("starting...", .{}); - const register_schema_file = try std.fs.cwd().openFile(state.register_schema_path, .{}); - defer register_schema_file.close(); - const text = try register_schema_file.readToEndAlloc(gpa, 1024 * 1024); - defer gpa.free(text); - - state.register_schema_usages = try std.json.parseFromSlice([]const RegisterSchemaUsage, gpa, text, .{ - .allocate = .alloc_always, - }); - - //var it = try serial.list(); - //defer it.deinit(); - //while (try it.next()) |info| { - // tl2.addText(try arena.allocator().dupe(u8, info.display_name), .{}); - // tl2.addText("\n\n", .{}); - //} + // Register schemas are now embedded at compile time via the schemas module + // No runtime file loading or JSON parsing needed! } // Run as app is shutting down before dvui.Window.deinit() @@ -166,10 +151,7 @@ fn show_stats_window() void { if (!state.show_stats_window) return; - if (state.register_schema_usages == null) - return; - - const rsus = state.register_schema_usages.?.value; + const rsus = schemas.schemas; const stats = compute_stats(rsus); var float = dvui.floatingWindow(@src(), .{ .open_flag = &state.show_stats_window }, .{ @@ -369,7 +351,7 @@ fn from_microzig_menu() void { if (row_clicked) |row_num| { std.log.info("clicked row: {}", .{row_num}); - const register_schema = state.register_schema_usages.?.value[row_num]; + const register_schema = schemas.schemas[row_num]; // Check if there are multiple chips/targets for this register schema if (register_schema.chips.len > 1) { @@ -421,57 +403,55 @@ fn from_microzig_menu() void { highlight_style.processEvents(grid); - if (state.register_schema_usages) |rsus| { - for (rsus.value, 0..) |rsu, row_num| { - var cell_num: dvui.GridWidget.Cell = .colRow(0, row_num); - switch (rsu.location) { - .src_path => |src| { - { - defer cell_num.col_num += 1; - var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); - defer cell.deinit(); - - dvui.labelNoFmt(@src(), src.port_name, .{}, .{}); - } - { - defer cell_num.col_num += 1; - var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); - defer cell.deinit(); - - dvui.labelNoFmt(@src(), "", .{}, .{}); - } - { - defer cell_num.col_num += 1; - var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); - defer cell.deinit(); - - dvui.labelNoFmt(@src(), src.sub_path, .{}, .{}); - } - }, - .dependency => |dep| { - { - defer cell_num.col_num += 1; - var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); - defer cell.deinit(); - - dvui.labelNoFmt(@src(), dep.port_name, .{}, .{}); - } - { - defer cell_num.col_num += 1; - var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); - defer cell.deinit(); - - dvui.labelNoFmt(@src(), dep.dep_name, .{}, .{}); - } - { - defer cell_num.col_num += 1; - var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); - defer cell.deinit(); - - dvui.labelNoFmt(@src(), dep.sub_path, .{}, .{}); - } - }, - } + for (schemas.schemas, 0..) |rsu, row_num| { + var cell_num: dvui.GridWidget.Cell = .colRow(0, row_num); + switch (rsu.location) { + .src_path => |src| { + { + defer cell_num.col_num += 1; + var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); + defer cell.deinit(); + + dvui.labelNoFmt(@src(), src.port_name, .{}, .{}); + } + { + defer cell_num.col_num += 1; + var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); + defer cell.deinit(); + + dvui.labelNoFmt(@src(), "", .{}, .{}); + } + { + defer cell_num.col_num += 1; + var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); + defer cell.deinit(); + + dvui.labelNoFmt(@src(), src.sub_path, .{}, .{}); + } + }, + .dependency => |dep| { + { + defer cell_num.col_num += 1; + var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); + defer cell.deinit(); + + dvui.labelNoFmt(@src(), dep.port_name, .{}, .{}); + } + { + defer cell_num.col_num += 1; + var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); + defer cell.deinit(); + + dvui.labelNoFmt(@src(), dep.dep_name, .{}, .{}); + } + { + defer cell_num.col_num += 1; + var cell = grid.bodyCell(@src(), cell_num, highlight_style.cellOptions(cell_num)); + defer cell.deinit(); + + dvui.labelNoFmt(@src(), dep.sub_path, .{}, .{}); + } + }, } } } @@ -527,9 +507,9 @@ fn search_chips_window() void { }; if (row_clicked) |clicked| { - const rsus = state.register_schema_usages orelse return; - if (clicked.rsu_idx >= rsus.value.len) return; - const rsu = rsus.value[clicked.rsu_idx]; + const rsus = schemas.schemas; + if (clicked.rsu_idx >= rsus.len) return; + const rsu = rsus[clicked.rsu_idx]; if (clicked.chip_idx >= rsu.chips.len) return; const chip = rsu.chips[clicked.chip_idx]; @@ -562,9 +542,9 @@ fn search_chips_window() void { const max_results: usize = 50; - if (state.register_schema_usages) |rsus| { + { var row_num: usize = 0; - outer: for (rsus.value) |rsu| { + outer: for (schemas.schemas) |rsu| { const port_name = switch (rsu.location) { .src_path => |loc| loc.port_name, .dependency => |loc| loc.port_name, @@ -648,9 +628,9 @@ fn find_chip_by_row(query: []const u8, target_row: usize) ?ChipLocation { const max_results: usize = 50; - if (state.register_schema_usages) |rsus| { + { var row_num: usize = 0; - outer: for (rsus.value, 0..) |rsu, rsu_idx| { + outer: for (schemas.schemas, 0..) |rsu, rsu_idx| { for (rsu.chips, 0..) |chip, chip_idx| { // Limit to max results if (row_num >= max_results) break :outer; @@ -677,9 +657,9 @@ fn find_chip_by_row(query: []const u8, target_row: usize) ?ChipLocation { } fn count_targets_for_chip(chip_name: []const u8) usize { - const rsus = state.register_schema_usages orelse return 0; + const rsus = schemas.schemas; var count: usize = 0; - for (rsus.value) |rsu| { + for (rsus) |rsu| { for (rsu.chips) |chip| { if (std.mem.eql(u8, chip.name, chip_name)) { count += 1; @@ -690,10 +670,10 @@ fn count_targets_for_chip(chip_name: []const u8) usize { } fn open_chip_target(rsu_idx: usize, chip_idx: usize) void { - const rsus = state.register_schema_usages orelse return; - if (rsu_idx >= rsus.value.len) return; + const rsus = schemas.schemas; + if (rsu_idx >= rsus.len) return; - const rsu = rsus.value[rsu_idx]; + const rsu = rsus[rsu_idx]; if (chip_idx >= rsu.chips.len) return; const chip = rsu.chips[chip_idx]; @@ -768,10 +748,10 @@ fn target_selection_window() void { highlight_style.processEvents(grid); - const rsus = state.register_schema_usages orelse return; + const rsus = schemas.schemas; var row_num: usize = 0; - for (rsus.value) |rsu| { + for (rsus) |rsu| { for (rsu.chips) |chip| { if (!std.mem.eql(u8, chip.name, state.selected_chip_name)) { continue; @@ -792,10 +772,10 @@ fn target_selection_window() void { } fn find_target_by_row(chip_name: []const u8, target_row: usize) ?TargetLocation { - const rsus = state.register_schema_usages orelse return null; + const rsus = schemas.schemas; var row_num: usize = 0; - for (rsus.value, 0..) |rsu, rsu_idx| { + for (rsus, 0..) |rsu, rsu_idx| { for (rsu.chips, 0..) |chip, chip_idx| { if (!std.mem.eql(u8, chip.name, chip_name)) { continue; @@ -815,9 +795,9 @@ fn rsu_target_selection_window() void { return; const rsu_idx = state.selected_rsu_idx orelse return; - const rsus = state.register_schema_usages orelse return; - if (rsu_idx >= rsus.value.len) return; - const rsu = rsus.value[rsu_idx]; + const rsus = schemas.schemas; + if (rsu_idx >= rsus.len) return; + const rsu = rsus[rsu_idx]; var float = dvui.floatingWindow(@src(), .{ .open_flag = &state.show_rsu_target_selection_window }, .{ .min_size_content = .{ .w = 400, .h = 300 }, @@ -938,9 +918,9 @@ fn search_boards_window() void { const max_results: usize = 50; - if (state.register_schema_usages) |rsus| { + { var row_num: usize = 0; - outer: for (rsus.value) |rsu| { + outer: for (schemas.schemas) |rsu| { const port_name = switch (rsu.location) { .src_path => |loc| loc.port_name, .dependency => |loc| loc.port_name, @@ -1008,9 +988,9 @@ fn find_board_by_row(query: []const u8, target_row: usize) ?BoardLocation { const max_results: usize = 50; - if (state.register_schema_usages) |rsus| { + { var row_num: usize = 0; - outer: for (rsus.value, 0..) |rsu, rsu_idx| { + outer: for (schemas.schemas, 0..) |rsu, rsu_idx| { for (rsu.boards, 0..) |board, board_idx| { // Limit to max results if (row_num >= max_results) break :outer; @@ -1037,10 +1017,10 @@ fn find_board_by_row(query: []const u8, target_row: usize) ?BoardLocation { } fn open_board(rsu_idx: usize, board_idx: usize) void { - const rsus = state.register_schema_usages orelse return; - if (rsu_idx >= rsus.value.len) return; + const rsus = schemas.schemas; + if (rsu_idx >= rsus.len) return; - const rsu = rsus.value[rsu_idx]; + const rsu = rsus[rsu_idx]; if (board_idx >= rsu.boards.len) return; // Boards don't have their own chip info, so we use the first chip from the schema if available @@ -1138,9 +1118,9 @@ fn search_targets_window() void { const max_results: usize = 50; - if (state.register_schema_usages) |rsus| { + { var row_num: usize = 0; - outer: for (rsus.value) |rsu| { + outer: for (schemas.schemas) |rsu| { for (rsu.chips) |chip| { // Limit to max results if (row_num >= max_results) break :outer; @@ -1194,9 +1174,9 @@ fn find_target_by_query_row(query: []const u8, target_row: usize) ?TargetLocatio const max_results: usize = 50; - if (state.register_schema_usages) |rsus| { + { var row_num: usize = 0; - outer: for (rsus.value, 0..) |rsu, rsu_idx| { + outer: for (schemas.schemas, 0..) |rsu, rsu_idx| { for (rsu.chips, 0..) |chip, chip_idx| { // Limit to max results if (row_num >= max_results) break :outer;