const std = @import("std"); const lang = @import("lang.zig"); const images = @import("image.zig"); const plugin = @import("plugin.zig"); const custom = @import("custom.zig"); const magick = @import("magick.zig"); const log = std.log.scoped(.scritcher_runner); const Position = plugin.Position; const ParamList = plugin.ParamList; const ParamMap = plugin.ParamMap; const Image = images.Image; pub const RunError = error{ UnknownCommand, NoBMP, ImageRequired, }; pub const Runner = struct { allocator: std.mem.Allocator, /// The currently opened image in the runner image: ?*Image = null, /// If the runner is in REPL mode repl: bool, args: []const [:0]u8, pub fn init(allocator: std.mem.Allocator, repl: bool) Runner { return Runner{ .allocator = allocator, .repl = repl, .args = std.process.argsAlloc(allocator) catch unreachable, }; } pub fn deinit(self: *Runner) void { if (self.image) |image| { image.close(); } std.process.argsFree(self.allocator, self.args); } pub fn clone(self: *Runner) !Runner { var cloned_image = if (self.image) |image| try image.clone() else null; return Runner{ .allocator = self.allocator, .image = cloned_image, .repl = self.repl, .args = self.args, }; } fn resolveArg(self: *Runner, load_path: []const u8) ![]const u8 { std.debug.assert(load_path.len > 0); if (load_path[0] == ':') { // parse the index from 1 to end var index = try std.fmt.parseInt(usize, load_path[1..], 10); // if it isn't in the repl, args look like this: // 'scritcher ./script ./image' // if it is, it looks like this // 'scritcher repl ./script ./image' // ':0' should ALWAYS point to the image. if (self.repl) index += 3 else index += 3; for (self.args) |arg, idx| { log.debug("arg{d} = {s}\n", .{ idx, arg }); } log.debug("fetch arg idx={d}, val={s}\n", .{ index, self.args[index] }); return self.args[index]; } else { return load_path; } } // Caller owns returned memory. fn resolveArgPath(self: *Runner, path_or_argidx: []const u8) ![]const u8 { const path = try self.resolveArg(path_or_argidx); const resolved_path = try std.fs.path.resolve( self.allocator, &[_][]const u8{path}, ); return resolved_path; } fn loadCmd(self: *Runner, path_or_argidx: []const u8) !void { const load_path = try self.resolveArgPath(path_or_argidx); log.debug("\tload path: {s}\n", .{load_path}); // we could use ImageMagick to convert from X to BMP // but i can't find an easy way to do things in memory. // the upside is that this allows some pre-processing by the user // before loading the file into scritcher. for example, you can start // krita/gimp and make it export a bmp and while in the program you can // apply filters, etc. if (!std.mem.endsWith(u8, load_path, ".bmp") and !std.mem.endsWith(u8, load_path, ".ppm")) { log.debug("Only BMP files are allowed to be loaded. Got path '{s}'\n", .{load_path}); return RunError.NoBMP; } // we don't copy load_path into a temporary file because we're already // loading it under the SFM_READ mode, which won't cause any destructive // operations on the file. if (self.image) |image| image.close(); self.image = try Image.open(self.allocator, load_path); } fn getImage(self: *Runner) !*Image { if (self.image) |image| { return image; } else { log.debug("image is required!\n", .{}); return RunError.ImageRequired; } } /// Caller owns returned memory. fn makeGlitchedPath(self: *Runner) ![]const u8 { // we want to transform basename, if it is "x.bmp" to "x_gN.bmp", where // N is the maximum non-used integer. var image = try self.getImage(); const basename = std.fs.path.basename(image.path); const dirname = std.fs.path.dirname(image.path).?; var dir = try std.fs.cwd().openIterableDir(dirname, .{}); defer dir.close(); const period_idx = std.mem.lastIndexOf(u8, basename, ".").?; const extension = basename[period_idx..basename.len]; // starts_with would be "x_g", we want to find all files in the directory // that start with that name. const starts_with = try std.fmt.allocPrint(self.allocator, "{s}_g", .{ basename[0..period_idx], }); defer self.allocator.free(starts_with); var max: usize = 0; var it = dir.iterate(); while (try it.next()) |entry| { switch (entry.kind) { .File => blk: { if (!std.mem.startsWith(u8, entry.name, starts_with)) break :blk {}; // we want to get the N in x_gN.ext const entry_gidx = std.mem.lastIndexOf(u8, entry.name, "_g").?; const entry_pidx_opt = std.mem.lastIndexOf(u8, entry.name, "."); if (entry_pidx_opt == null) break :blk {}; const entry_pidx = entry_pidx_opt.?; // if N isn't a number, we just ignore that file const idx_str = entry.name[entry_gidx + 2 .. entry_pidx]; const idx = std.fmt.parseInt(usize, idx_str, 10) catch |err| { log.debug("ignoring file {s}", .{@errorName(err)}); break :blk {}; }; if (idx > max) max = idx; }, else => {}, } } const out_path = try std.fmt.allocPrint(self.allocator, "{s}/{s}{d}{s}", .{ dirname, starts_with, max + 1, extension, }); return out_path; } fn quicksaveCmd(self: *Runner) !void { var image = try self.getImage(); const out_path = try self.makeGlitchedPath(); defer self.allocator.free(out_path); try image.saveTo(out_path); } fn runQSCmd(self: *Runner, cmd: lang.Command) !void { const runqs = cmd.cast(lang.Command.RunQS).?; var image = try self.getImage(); const out_path = try self.makeGlitchedPath(); defer self.allocator.free(out_path); try image.saveTo(out_path); var proc = std.ChildProcess.init( &[_][]const u8{ runqs.program, out_path }, self.allocator, ); //defer proc.deinit(); log.debug("running '{s} {s}'\n", .{ runqs.program, out_path }); _ = try proc.spawnAndWait(); } fn rotateCmd(self: *Runner, cmd: lang.Command) !void { const rotate_cmd = cmd.cast(lang.Command.Rotate).?; var image = try self.getImage(); var c_bgfill = try std.cstr.addNullByte(self.allocator, rotate_cmd.bgfill); defer self.allocator.free(c_bgfill); try magick.runRotate(image, rotate_cmd.deg, c_bgfill); } fn executeLV2Command(self: *@This(), command: anytype) !void { const pos = plugin.Position{ .split = command.split, .index = command.index, }; var params = ParamList.init(self.allocator); defer params.deinit(); const typ = @TypeOf(command); inline for (@typeInfo(@TypeOf(command.parameters)).Struct.fields) |cmd_field| { try params.append(plugin.Param{ .sym = cmd_field.name, .value = @field(command.parameters, cmd_field.name), }); } var image = try self.getImage(); try image.runPlugin(typ.lv2_url, pos, params); } fn executeCustomCommand(self: *@This(), command: anytype) !void { const pos = plugin.Position{ .split = command.split, .index = command.index, }; var image = try self.getImage(); try image.runCustomPlugin(@TypeOf(command).plugin_type, pos, command.parameters); } fn runSingleCommand( self: *@This(), cmd: lang.Command, comptime tag: lang.Command.Tag, ) !void { const typ = lang.Command.tagToType(tag); const command = cmd.cast(typ).?; const ctype = typ.command_type; switch (ctype) { .lv2_command => try self.executeLV2Command(command.*), .custom_command => try self.executeCustomCommand(command.*), } } fn runCommand(self: *@This(), cmd: lang.Command) !void { switch (cmd.tag) { .noop => {}, .load => { const command = cmd.cast(lang.Command.Load).?; try self.loadCmd(command.path); }, .quicksave => try self.quicksaveCmd(), .rotate => try self.rotateCmd(cmd), .runqs => try self.runQSCmd(cmd), .amp => try self.runSingleCommand(cmd, .amp), .rflanger => try self.runSingleCommand(cmd, .rflanger), .eq => try self.runSingleCommand(cmd, .eq), .phaser => try self.runSingleCommand(cmd, .phaser), .mbeq => try self.runSingleCommand(cmd, .mbeq), .chorus => try self.runSingleCommand(cmd, .chorus), .pitchscaler => try self.runSingleCommand(cmd, .pitchscaler), .reverb => try self.runSingleCommand(cmd, .reverb), .highpass => try self.runSingleCommand(cmd, .highpass), .delay => try self.runSingleCommand(cmd, .delay), .vinyl => try self.runSingleCommand(cmd, .vinyl), .revdelay => try self.runSingleCommand(cmd, .revdelay), .gate => try self.runSingleCommand(cmd, .gate), .detune => try self.runSingleCommand(cmd, .detune), .overdrive => try self.runSingleCommand(cmd, .overdrive), .degrade => try self.runSingleCommand(cmd, .degrade), .repsycho => try self.runSingleCommand(cmd, .repsycho), .talkbox => try self.runSingleCommand(cmd, .talkbox), .dyncomp => try self.runSingleCommand(cmd, .dyncomp), .thruzero => try self.runSingleCommand(cmd, .thruzero), .foverdrive => try self.runSingleCommand(cmd, .foverdrive), .gverb => try self.runSingleCommand(cmd, .gverb), .invert => try self.runSingleCommand(cmd, .invert), .tapedelay => try self.runSingleCommand(cmd, .tapedelay), .moddelay => try self.runSingleCommand(cmd, .moddelay), .multichorus => try self.runSingleCommand(cmd, .multichorus), .saturator => try self.runSingleCommand(cmd, .saturator), .vintagedelay => try self.runSingleCommand(cmd, .vintagedelay), .noise => try self.runSingleCommand(cmd, .noise), .wildnoise => try self.runSingleCommand(cmd, .wildnoise), .write => try self.runSingleCommand(cmd, .write), .embed => try self.runSingleCommand(cmd, .embed), } } /// Run a list of commands. pub fn runCommands( self: *Runner, cmds: lang.CommandList, debug_flag: bool, ) !void { _ = debug_flag; for (cmds.list.items) |cmd| { cmd.print(); try self.runCommand(cmd.*); } } }; test "running noop" { const allocator = std.testing.allocator; var cmds = lang.CommandList.init(allocator); defer cmds.deinit(); var command = lang.Command{ .tag = .noop }; var noop = try allocator.create(lang.Command.Noop); noop.* = lang.Command.Noop{ .base = command }; try cmds.append(&noop.base); var runner = Runner.init(allocator, false); defer runner.deinit(); try runner.runCommands(cmds, false); }