diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | LICENSE | 28 | ||||
-rw-r--r-- | build.zig | 43 | ||||
-rw-r--r-- | build.zig.zon | 11 | ||||
-rw-r--r-- | flake.lock | 129 | ||||
-rw-r--r-- | flake.nix | 27 | ||||
-rw-r--r-- | pick.zig | 184 |
7 files changed, 424 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e73c965 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-cache/ +zig-out/ @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2023, Kitty-Cricket Piapiac + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..f9bae63 --- /dev/null +++ b/build.zig @@ -0,0 +1,43 @@ +const std = @import("std"); +const dvui = @import("dvui"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const dvui_dep = b.dependency("dvui", .{ + .target = target, + .optimize = optimize, + }); + + const exe = b.addExecutable(.{ + .name = "pick", + .root_source_file = .{ .path = "pick.zig" }, + .target = target, + .optimize = optimize, + }); + + exe.addModule("dvui", dvui_dep.module("dvui")); + exe.addModule("SDLBackend", dvui_dep.module("SDLBackend")); + + dvui.link_deps(exe, dvui_dep.builder); + 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 unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "pick.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..f9365d0 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = "pick", + .version = "0.0.1", + .paths = .{ "." }, + .dependencies = .{ + .dvui = .{ + .url = "https://git.disroot.org/kcp/dvui/archive/cb349fd04e983aded2b552ad0c746c32dd22778e.tar.gz", + .hash = "12206ea27c2bd175d16911e569c8d71f1a1005d3c5aef4feb841b08d404ab4176c91", + }, + }, +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f386b09 --- /dev/null +++ b/flake.lock @@ -0,0 +1,129 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1699099776, + "narHash": "sha256-X09iKJ27mGsGambGfkKzqvw5esP1L/Rf8H3u3fCqIiU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "85f1ba3e51676fa8cc604a3d863d729026a6b8eb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1689088367, + "narHash": "sha256-Y2tl2TlKCWEHrOeM9ivjCLlRAKH3qoPUE/emhZECU14=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5c9ddb86679c400d6b7360797b8a22167c2053f8", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-23.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "zig": "zig" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1699790839, + "narHash": "sha256-ooq3+JFM7dUop2td9B0HvHouEIp1llJkjayumEQhbAI=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "0d94a374db81dbdd9b0039c9378b2ece9d2a12d4", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..162247d --- /dev/null +++ b/flake.nix @@ -0,0 +1,27 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + zig.url = "github:mitchellh/zig-overlay"; + }; + outputs = { self, nixpkgs, flake-utils, zig }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ + zig.overlays.default + ]; + }; + in { + devShells.default = pkgs.mkShell { + buildInputs = [ + pkgs.SDL2 + pkgs.SDL2.dev + pkgs.pkg-config + pkgs.zigpkgs.master + ]; + }; + } + ); +} diff --git a/pick.zig b/pick.zig new file mode 100644 index 0000000..76b1213 --- /dev/null +++ b/pick.zig @@ -0,0 +1,184 @@ +const std = @import("std"); +const dvui = @import("dvui"); +const entypo = dvui.entypo; +const Backend = @import("SDLBackend"); + +var gpa_instance = std.heap.GeneralPurposeAllocator(.{}){}; +const gpa = gpa_instance.allocator(); +const vsync = true; + +pub fn main() !void { + var backend = try Backend.init(.{ + .width = 480, + .height = 360, + .vsync = vsync, + .title = "File Picker", + }); + defer backend.deinit(); + + var win = try dvui.Window.init(@src(), 0, gpa, backend.backend()); + defer win.deinit(); + win.theme = &dvui.Adwaita.light; + + var ctx = try Ctx.init(); + + loop: while (true) { + var nstime = win.beginWait(backend.hasEvent()); + try win.begin(nstime); + backend.clear(); + + const quit = try backend.addAllEvents(&win); + if (quit or .exit == try ctx.frame()) break :loop; + + const end_micros = try win.end(.{}); + backend.setCursor(win.cursorRequested()); + backend.renderPresent(); + + const wait_event_micros = win.waitTime(end_micros, null); + backend.waitEventTimeout(wait_event_micros); + } +} + +pub const Ctx = struct { + path: []u8 = "", + filename: []u8 = "", + entries: std.ArrayListUnmanaged(std.fs.IterableDir.Entry) = .{}, + + pub fn init() !Ctx { + var ret = Ctx{}; + try ret.list_files(); + return ret; + } + + pub const ExitStatus = enum { ok, exit }; + pub fn frame(ctx: *Ctx) !ExitStatus { + const bg = dvui.Options{ + .background = true, + .expand = .horizontal, + .corner_radius = dvui.Rect.all(0), + }; + + const opts = dvui.Options{ + .corner_radius = dvui.Rect.all(0), + }; + + var box = try dvui.box(@src(), .vertical, bg.override(.{ + .expand = .both, + })); + defer box.deinit(); + + { + var top = try dvui.box(@src(), .horizontal, bg); + defer top.deinit(); + + if (try dvui.button(@src(), "..", opts)) { + try std.os.chdir(".."); + try ctx.list_files(); + } + + const te = try dvui.textEntry(@src(), .{ .text = ctx.path }, bg); + defer te.deinit(); + } + { + var bottom = try dvui.box(@src(), .horizontal, opts.override(.{ + .expand = .horizontal, + .gravity_y = 1, + })); + defer bottom.deinit(); + + const button_enabled = ctx.filename.len != 0; + var button_opts = opts.override(.{ .gravity_x = 1 }); + if (button_enabled) { + button_opts.color_style = .accent; + } else { + button_opts.color_style = .control; + button_opts.color_hover = button_opts.color(.fill); + } + if (try dvui.button(@src(), "ok", button_opts) and button_enabled) { + std.debug.print("{s}/{s}\n", .{ ctx.path, ctx.filename }); + return .exit; + } + + const tl = try dvui.textEntry(@src(), .{ .text = ctx.filename }, opts.override(.{ + .expand = .horizontal, + })); + defer tl.deinit(); + } + { + var scroll = try dvui.scrollArea(@src(), .{}, .{ .expand = .both }); + defer scroll.deinit(); + + for (ctx.entries.items, 0..) |entry, i| { + const name, const icon, const color_style: dvui.Theme.ColorStyle = switch (entry.kind) { + .directory => .{ "folder", entypo.folder, .window }, + .file => .{ "file", entypo.text_document, .control }, + else => .{ "other", entypo.help, .content }, + }; + + const id = opts.override(.{ .id_extra = i }); + const wide = id.override(.{ + .color_style = color_style, + .expand = .horizontal, + .margin = dvui.Rect.all(0), + }); + + var bw = dvui.ButtonWidget.init(@src(), .{}, wide); + try bw.install(); + bw.processEvents(); + try bw.drawBackground(); + + const m = try dvui.menu(@src(), .horizontal, id); + try dvui.icon(@src(), name, icon, id.override(.{ .gravity_y = 0.4 })); + try dvui.labelNoFmt(@src(), entry.name, id); + m.deinit(); + + var clicked = bw.clicked(); + try bw.drawFocus(); + bw.deinit(); + + if (clicked) switch (entry.kind) { + .directory => { + try std.os.chdir(entry.name); + try ctx.list_files(); + }, + else => { + gpa.free(ctx.filename); + ctx.filename = try gpa.dupe(u8, entry.name); + }, + }; + } + } + + return .ok; + } + + pub fn list_files(ctx: *Ctx) !void { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const path = try std.os.realpath(".", &buf); + ctx.path = try gpa.dupe(u8, path); + + const cwd = std.fs.cwd(); + const iterable = try cwd.openIterableDir(".", .{}); + var iterator = iterable.iterate(); + + ctx.entries.clearRetainingCapacity(); + + while (try iterator.next()) |entry| { + const name = try gpa.dupe(u8, entry.name); + try ctx.entries.append(gpa, .{ .name = name, .kind = entry.kind }); + } + + const Entry = std.fs.IterableDir.Entry; + const items = ctx.entries.items; + const closures = struct { + pub fn less_than_0(_: void, l: Entry, r: Entry) bool { + return std.mem.lessThan(u8, l.name, r.name); + } + pub fn less_than_1(_: void, l: Entry, r: Entry) bool { + return @intFromEnum(l.kind) < @intFromEnum(r.kind); + } + }; + std.mem.sort(Entry, items, {}, closures.less_than_0); + std.mem.sort(Entry, items, {}, closures.less_than_1); + } +}; |