commit e1d3d98fed17e46acf7825de91254684bac4e84f Author: Dario48 Date: Sat Aug 30 15:26:01 2025 +0200 guh (add everything) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed4cf88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +zig-out +.zig-cache +.DS_Store diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..897f764 --- /dev/null +++ b/build.zig @@ -0,0 +1,66 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + + const optimize = b.standardOptimizeOption(.{}); + + const lib_mod = b.createModule(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + const exe_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + exe_mod.addImport("taggr_lib", lib_mod); + + const lib = b.addLibrary(.{ + .linkage = .static, + .name = "taggr", + .root_module = lib_mod, + }); + + b.installArtifact(lib); + + const exe = b.addExecutable(.{ + .name = "taggr", + .root_module = exe_mod, + }); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const lib_unit_tests = b.addTest(.{ + .root_module = lib_mod, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const exe_unit_tests = b.addTest(.{ + .root_module = exe_mod, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..0f81a44 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,86 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = .taggr, + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // Together with name, this represents a globally unique package + // identifier. This field is generated by the Zig toolchain when the + // package is first created, and then *never changes*. This allows + // unambiguous detection of one package being an updated version of + // another. + // + // When forking a Zig project, this id should be regenerated (delete the + // field and run `zig build`) if the upstream project is still maintained. + // Otherwise, the fork is *hostile*, attempting to take control over the + // original project's identity. Thus it is recommended to leave the comment + // on the following line intact, so that it shows up in code reviews that + // modify the field. + .fingerprint = 0x90c511a182377f7b, // Changing this has security and trust implications. + + // Tracks the earliest Zig version that the package considers to be a + // supported use case. + .minimum_zig_version = "0.14.1", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. If the contents of a URL change this will result in a hash mismatch + // // which will prevent zig from using it. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + // + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..6054e5e --- /dev/null +++ b/src/main.zig @@ -0,0 +1,14 @@ +//! cli application to tag files +//! todo: make it work + +pub fn main() !void { + var tags = try lib.retrieve_tags(.{}); + defer lib.deallocate_top_tag(&tags); + + var image = try lib.get_images(tags.tags[0]); + defer image.deinit(); +} + +const std = @import("std"); + +const lib = @import("taggr_lib"); diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..5c45879 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,246 @@ +//! library that tags photos and keeps track of them +//! todo: implement +const std = @import("std"); +const builtin = @import("builtin"); + +pub const errors = error{UnsupportedPlatform}; + +pub const retrieve_options = struct { + pub const tags = struct { + path: ?std.fs.Dir = null, + allocator: std.mem.Allocator = std.heap.smp_allocator, + }; +}; + +const top_tag = struct { + allocator: std.mem.Allocator, + path: std.fs.Dir, + tags: []tag, + zon: zon_struct, +}; + +const tag = struct { + name: []const u8, + parent: *top_tag, + children: ?[]tag, +}; + +const zon_struct = struct { + name: []const u8, + children: ?[]zon_struct, +}; + +const zon_file_struct = struct { + name: []u8, + filename: []u8, +}; + +const zon_tag_struct = struct { + files: []zon_file_struct, +}; + +pub fn retrieve_tags(options: retrieve_options.tags) !top_tag { + var buffer: [1024]u8 = undefined; + const conf_dir: std.fs.Dir = if (options.path) |user_conf_dir| user_conf_dir else switch (builtin.os.tag) { + .windows => blk: { + const homedir = try std.process.getEnvVarOwned(options.allocator, "APPDATA"); + defer options.allocator.free(homedir); + const home_dir = try std.fs.openDirAbsolute(homedir, .{}); + break :blk home_dir.openDir("taggr", .{}) catch |open_dir_err| switch (open_dir_err) { + error.FileNotFound => { + std.log.debug("FileNotFound, falling back to makeOpenPath", .{}); + try home_dir.makeOpenPath("taggr", .{}); + }, + else => return open_dir_err, + }; + }, + .linux, + .openbsd, + .freebsd, + .dragonfly, + .netbsd, + .macos, + => blk: { + const homedir = try std.process.getEnvVarOwned(options.allocator, "HOME"); + defer options.allocator.free(homedir); + const home_dir = try std.fs.openDirAbsolute(homedir, .{}); + break :blk home_dir.openDir(".local/share/taggr", .{}) catch |open_dir_err| switch (open_dir_err) { + error.FileNotFound => blk_inner: { + std.log.debug("FileNotFound, falling back to makeOpenPath", .{}); + break :blk_inner try home_dir.makeOpenPath(".local/share/taggr", .{}); + }, + else => return open_dir_err, + }; + }, + else => return errors.UnsupportedPlatform, + }; + + std.log.debug("path: {s}", .{try conf_dir.realpath(".", &buffer)}); + + const conf_path = if (options.path) |config_path| config_path.openFile("config.zon", .{}) catch |open_file_err| switch (open_file_err) { + error.FileNotFound => try config_path.createFile("config.zon", .{}), + else => return open_file_err, + } else conf_dir.createFile("config.zon", .{}) catch |open_file_err| switch (open_file_err) { + error.FileNotFound => blk: { + std.log.debug("FileNotFound, falling back to createFile", .{}); + break :blk try conf_dir.createFile("config.zon", .{}); + }, + else => return open_file_err, + }; + + var initTag = top_tag{ + .tags = undefined, + .allocator = options.allocator, + .path = conf_dir, + .zon = undefined, + }; + + var diag = std.zon.parse.Diagnostics{}; + + var zon_file = std.Io.Writer.Allocating.init(options.allocator); + + var conf_path_buffer: [1024]u8 = undefined; + var reader = conf_path.reader(&conf_path_buffer).interface; + + var stdout_buffer: [1024]u8 = undefined; + var stdout = std.fs.File.stdout().writer(&stdout_buffer); + + _ = reader.stream(&stdout.interface, .unlimited) catch |stream_error| switch (stream_error) { + error.EndOfStream => {}, + else => return stream_error, + }; + + try stdout.interface.flush(); + + _ = reader.stream(&zon_file.writer, .unlimited) catch |stream_error| switch (stream_error) { + error.EndOfStream => {}, + else => return stream_error, + }; + + const zon_file_written = zon_file.written(); + + const zon = std.zon.parse.fromSlice(zon_struct, options.allocator, zon_file_written[0 .. zon_file_written.len - 1 :0], &diag, .{}) catch |zon_error| switch (zon_error) { + error.ParseZon => { + var Writer = std.Io.Writer.Allocating.init(options.allocator); + try diag.format(&Writer.writer); + std.log.err("\n", .{}); + std.log.err("{s}", .{Writer.written()}); + diag.deinit(options.allocator); + Writer.deinit(); + return zon_error; + }, + else => return zon_error, + }; + + conf_path.close(); + + zon_file.deinit(); + + initTag.tags = try parse_zon_tag_root(zon, options.allocator, &initTag); + + return initTag; +} + +fn parse_zon_tag_root(zon_root: zon_struct, allocator: std.mem.Allocator, parent: *top_tag) ![]tag { + return (try optional_zon_struct_slice_to_optional_tag_slice(zon_root.children, allocator, parent)).?; +} + +fn optional_zon_struct_slice_to_optional_tag_slice(zon_tags_optional: ?[]zon_struct, allocator: std.mem.Allocator, parent: *top_tag) !?[]tag { + if (zon_tags_optional) |zon_tags| { + var gen_tags = try allocator.alloc(tag, zon_tags.len); + for (zon_tags, 0..) |zon_tag, i| { + gen_tags[i] = tag{ + .children = try optional_zon_struct_slice_to_optional_tag_slice(zon_tag.children, allocator, parent), + .parent = parent, + .name = zon_tag.name, + }; + } + return gen_tags; + } + return null; +} + +pub fn deallocate_top_tag(allocated_tag: *top_tag) void { + deallocate_tag(allocated_tag.tags); + std.zon.parse.free(allocated_tag.allocator, allocated_tag.zon); + allocated_tag.path.close(); +} + +fn deallocate_tag(to_deallocate: ?[]tag) void { + if (to_deallocate) |children| { + for (children) |child| { + deallocate_tag(child.children); + } + children[0].parent.allocator.free(children); + } +} + +const images = struct { + pub const image = struct { + path: []const u8, + name: []const u8, + }; + paths: []image, + allocator: std.mem.Allocator, + pub fn deinit(self: images) void { + self.allocator.free(self.paths); + } +}; + +/// returns the path to the images tagged with the given tag +/// the resoult needs to be deallocated with deinit +pub fn get_images(needed_tag: tag) !images { + const allocator = needed_tag.parent.allocator; + if (needed_tag.children) |children| { + var image_arraylist = std.ArrayList(images).empty; + + for (children) |child| { + try image_arraylist.append(allocator, try get_images(child)); + } + + const image_array = try image_arraylist.toOwnedSlice(allocator); + var image_names_arraylist = std.ArrayList(images.image).empty; + + for (image_array) |image| { + try image_names_arraylist.appendSlice(allocator, image.paths); + image.deinit(); + } + + allocator.free(image_array); + + return images{ + .allocator = allocator, + .paths = try image_names_arraylist.toOwnedSlice(allocator), + }; + } + + var buffer: [1024]u8 = undefined; + + const filename: [:0]u8 = @ptrCast(try std.fmt.allocPrintSentinel(allocator, switch (@import("builtin").os.tag) { + .windows => "{s}\\{s}", + .linux, + .openbsd, + .freebsd, + .dragonfly, + .netbsd, + .macos, + => "{s}/{s}", + else => return errors.UnsupportedPlatform, + }, .{ try needed_tag.parent.path.realpath(".", &buffer), needed_tag.name }, 0)); + const zon_tag = try std.zon.parse.fromSlice(zon_tag_struct, allocator, filename, null, .{}); + var paths = try allocator.alloc(images.image, zon_tag.files.len); + + for (zon_tag.files, 0..) |file, i| { + paths[i] = images.image{ + .name = file.name, + .path = try needed_tag.parent.path.realpath(file.filename, &buffer), + }; + } + + std.zon.parse.free(allocator, zon_tag); + + return images{ + .allocator = allocator, + .paths = paths, + }; +}