scritcher/src/runner.zig

346 lines
12 KiB
Zig

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}", .{ idx, arg });
}
log.debug("fetch arg idx={d}", .{index});
log.debug("fetch arg val={s}", .{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}", .{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}'", .{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!", .{});
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}'", .{ 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);
}