const std = @import("std"); const langs = @import("lang.zig"); const runners = @import("runner.zig"); const printer = @import("printer.zig"); const log = std.log.scoped(.scritcher); test "scritcher" { _ = @import("lang.zig"); _ = @import("runner.zig"); } const readline = @cImport({ @cInclude("stdio.h"); @cInclude("stdlib.h"); @cInclude("readline/readline.h"); @cInclude("readline/history.h"); }); fn wrapInCmdList(allocator: std.mem.Allocator, cmd: *langs.Command) !langs.CommandList { var cmds = langs.CommandList.init(allocator); try cmds.append(cmd); return cmds; } fn copyCommandToHeap(allocator: std.mem.Allocator, command: langs.Command, comptime tag: langs.Command.Tag) !*langs.Command { const CommandStruct = langs.Command.tagToType(tag); const casted = command.cast(CommandStruct).?; var heap_cmd = try allocator.create(CommandStruct); heap_cmd.* = casted.*; return &heap_cmd.base; } pub fn doRepl(allocator: std.mem.Allocator, args_it: anytype) !void { var stdout_file = std.io.getStdOut(); const stdout = &stdout_file.writer(); const scri_path = (args_it.next() orelse @panic("expected scri path")); errdefer allocator.free(scri_path); defer allocator.free(scri_path); var file_read_opt: ?std.fs.File = std.fs.cwd().openFile(scri_path, .{}) catch |err| blk: { if (err == error.FileNotFound) break :blk null; return err; }; const total_bytes = if (file_read_opt) |file_read| try file_read.getEndPos() else 0; var cmds = langs.CommandList.init(allocator); defer cmds.deinit(); var lang = langs.Lang.init(allocator); defer lang.deinit(); if (total_bytes > 0) { var scri_existing = try allocator.alloc(u8, total_bytes); _ = try file_read_opt.?.read(scri_existing); defer allocator.free(scri_existing); // we can't defer this directly because we copy the // Command pointers to the cmds list. running deinit() directly // would cause those pointers to be freed. var existing_cmds = try lang.parse(scri_existing); defer existing_cmds.list.deinit(); // copy the existing command list into the repl's command list for (existing_cmds.list.items) |existing_cmd| { try cmds.append(existing_cmd); } } else { // if there isn't any commands on the file, we load our default // 'load :0' command // TODO: deliberate memleak here. we only allocate this // command once, for the start of the file, so. var load_cmd = try allocator.create(langs.Command.Load); load_cmd.path = ":0"; load_cmd.base.tag = langs.Command.Tag.load; // taking address is fine, because load_cmd lives in the lifetime // of the allocator. try cmds.append(&load_cmd.base); } if (file_read_opt) |file_read| { file_read.close(); } var file = try std.fs.cwd().createFile(scri_path, .{ .read = false, .truncate = true, }); defer file.close(); var out = file.writer(); var stream = &out; // since we opened the file for writing, it becomes empty, so, to ensure // we don't fuck up later on, we print cmds before starting the repl try printer.printList(cmds, stdout); try printer.printList(cmds, stream); // we keep // - a CommandList with the full commands we have right now // - a Command with the current last typed successful command // - one runner that contains the current full state of the image // as if the current cmds was ran over it (TODO better wording) // - one runner that gets copied from the original on every new // command the user issues var current: langs.Command = undefined; var runner = runners.Runner.init(allocator, true); defer runner.deinit(); // run the load command try runner.runCommands(cmds, true); const wanted_runner: []const u8 = std.os.getenv("SCRITCHER_RUNNER") orelse "ristretto"; var runqs_cmd = langs.Command.RunQS{ .base = langs.Command{ .tag = langs.Command.Tag.runqs }, .program = wanted_runner, }; while (true) { lang.reset(); var rd_line = readline.readline("> "); if (rd_line == null) { log.debug("leaving from eof", .{}); break; } readline.add_history(rd_line); //defer std.heap.c_allocator.destroy(rd_line); var line = rd_line[0..std.mem.len(rd_line)]; if (std.mem.eql(u8, line, "push")) { const heap_cmd = switch (current.tag) { .noop => try copyCommandToHeap(allocator, current, .noop), .load => try copyCommandToHeap(allocator, current, .load), .quicksave => try copyCommandToHeap(allocator, current, .quicksave), .runqs => try copyCommandToHeap(allocator, current, .runqs), .amp => try copyCommandToHeap(allocator, current, .amp), .rflanger => try copyCommandToHeap(allocator, current, .rflanger), .eq => try copyCommandToHeap(allocator, current, .eq), .phaser => try copyCommandToHeap(allocator, current, .phaser), .mbeq => try copyCommandToHeap(allocator, current, .mbeq), .chorus => try copyCommandToHeap(allocator, current, .chorus), .pitchscaler => try copyCommandToHeap(allocator, current, .pitchscaler), .reverb => try copyCommandToHeap(allocator, current, .reverb), .highpass => try copyCommandToHeap(allocator, current, .highpass), .delay => try copyCommandToHeap(allocator, current, .delay), .vinyl => try copyCommandToHeap(allocator, current, .vinyl), .revdelay => try copyCommandToHeap(allocator, current, .revdelay), .gate => try copyCommandToHeap(allocator, current, .gate), .detune => try copyCommandToHeap(allocator, current, .detune), .overdrive => try copyCommandToHeap(allocator, current, .overdrive), .degrade => try copyCommandToHeap(allocator, current, .degrade), .repsycho => try copyCommandToHeap(allocator, current, .repsycho), .talkbox => try copyCommandToHeap(allocator, current, .talkbox), .dyncomp => try copyCommandToHeap(allocator, current, .dyncomp), .thruzero => try copyCommandToHeap(allocator, current, .thruzero), .foverdrive => try copyCommandToHeap(allocator, current, .foverdrive), .gverb => try copyCommandToHeap(allocator, current, .gverb), .invert => try copyCommandToHeap(allocator, current, .invert), .tapedelay => try copyCommandToHeap(allocator, current, .tapedelay), .moddelay => try copyCommandToHeap(allocator, current, .moddelay), .multichorus => try copyCommandToHeap(allocator, current, .multichorus), .saturator => try copyCommandToHeap(allocator, current, .saturator), .vintagedelay => try copyCommandToHeap(allocator, current, .vintagedelay), .noise => try copyCommandToHeap(allocator, current, .noise), .wildnoise => try copyCommandToHeap(allocator, current, .wildnoise), .write => try copyCommandToHeap(allocator, current, .write), .embed => try copyCommandToHeap(allocator, current, .embed), .rotate => try copyCommandToHeap(allocator, current, .rotate), }; try cmds.append(heap_cmd); // run the current added command to main cmds list // with the main parent runner var cmds_wrapped = try wrapInCmdList(allocator, heap_cmd); defer cmds_wrapped.deinit(); try runner.runCommands(cmds_wrapped, true); continue; } else if (std.mem.eql(u8, line, "list")) { try printer.printList(cmds, stdout); continue; } else if (std.mem.eql(u8, line, "save")) { // seek to 0 instead of appending the new command // NOTE appending single command might be faster try file.seekTo(0); try printer.printList(cmds, stream); continue; } else if (std.mem.eql(u8, line, "quit") or std.mem.eql(u8, line, "q")) { log.debug("leaving", .{}); break; } else if (std.mem.startsWith(u8, line, "#")) { continue; } var cmds_parsed = lang.parse(line) catch |err| { log.debug("repl: error while parsing: {}", .{err}); continue; }; // no command? ignore! if (cmds_parsed.list.items.len == 0) continue; current = cmds_parsed.list.items[0].*; // by cloning the parent runner, we can iteratively write // whatever command we want and only commit the good results // back to the parent runner var runner_clone = try runner.clone(); defer runner_clone.deinit(); // taking address is fine, because runqs_cmd lives in the lifetime // of this function. try cmds_parsed.append(&runqs_cmd.base); try runner_clone.runCommands(cmds_parsed, true); _ = try stdout.write("\n"); } } fn doHelp() void { log.debug("scritcher!", .{}); log.debug("usage: scritcher [run|help|repl]", .{}); log.debug("\tscritcher run path_to_script.scri path_to_input_file.bmp", .{}); log.debug("\tscritcher repl path_to_script.scri path_to_input_file.bmp", .{}); } fn doRun(allocator: std.mem.Allocator, args_it: anytype) !void { var lang = langs.Lang.init(allocator); defer lang.deinit(); var runner = runners.Runner.init(allocator, false); defer runner.deinit(); const scri_path = (args_it.next() orelse @panic("run: expected scri path")); var file = try std.fs.cwd().openFile(scri_path, .{}); defer file.close(); // sadly, we read it all into memory. such is life const total_bytes = try file.getEndPos(); var data = try allocator.alloc(u8, total_bytes); defer allocator.free(data); _ = try file.read(data); var cmds = try lang.parse(data); defer cmds.deinit(); try runner.runCommands(cmds, true); } pub fn main() !void { var allocator_instance = std.heap.GeneralPurposeAllocator(.{}){}; defer { _ = allocator_instance.deinit(); } const allocator = allocator_instance.allocator(); var args_it = try std.process.argsWithAllocator(allocator); defer args_it.deinit(); _ = args_it.skip(); const cli_command_opt = args_it.next(); if (cli_command_opt == null) { return doHelp(); } const cli_command = cli_command_opt.?; if (std.mem.eql(u8, cli_command, "help")) { return doHelp(); } else if (std.mem.eql(u8, cli_command, "repl")) { return try doRepl(allocator, &args_it); } else if (std.mem.eql(u8, cli_command, "run")) { return try doRun(allocator, &args_it); } else { log.debug("unknown command: '{s}'", .{cli_command}); return error.UnknownCommand; } }