From 4a5e316dd0c0c980d022f04a825e02b60f90a20f Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 15 Jul 2026 13:42:54 +0000 Subject: [PATCH] wip: init Co-authored-by: june --- .envrc | 1 + .gitignore | 7 +++++++ build.zig | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ build.zig.zon | 8 ++++++++ flake.lock | 27 +++++++++++++++++++++++++++ flake.nix | 23 +++++++++++++++++++++++ meow.sh | 8 ++++++++ readme.md | 3 +++ nix/deps.nix | 4 ++++ nix/package.nix | 34 ++++++++++++++++++++++++++++++++++ nix/shell.nix | 14 ++++++++++++++ src/ascii.txt | 4 ++++ src/cmd.zig | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/info.zig | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/root.zig | 25 +++++++++++++++++++++++++ src/strwidth.zig | 40 ++++++++++++++++++++++++++++++++++++++++ src/wchar.h | 2 ++ 18 file(s) changed, 534 insertion(s)(+), 0 deletion(s)(-) diff --git a/.envrc b/.envrc new file mode 100644 --- /dev/null +++ b/.envrc @@ -0,0 +1,1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# zig +/.zig-cache/ +/zig-out/ +/zig-pkg/ + +# dev +/.direnv/ diff --git a/build.zig b/build.zig new file mode 100644 --- /dev/null +++ b/build.zig @@ -0,0 +1,57 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const mod = b.addModule("cdl", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + const exe = b.addExecutable(.{ + .name = "cdl", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + + exe.root_module.addImport("cdl", mod); + + b.installArtifact(exe); + + // deps =================================================================== + + const c_wchar = b.addTranslateC(.{ + .root_source_file = b.path("src/wchar.h"), + .target = target, + .optimize = optimize, + }); + + mod.addImport("wchar", c_wchar.createModule()); + + // run ==================================================================== + + const run_step = b.step("run", "Run"); + + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // test =================================================================== + + const test_step = b.step("test", "Test"); + + const test_step_run = b.addRunArtifact(b.addTest(.{ + .root_module = mod, + })); + test_step.dependOn(&test_step_run.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,8 @@ +.{ + .name = .cdl, + .version = "0.0.1", + .dependencies = .{}, + .minimum_zig_version = "0.16.0", + .paths = .{""}, + .fingerprint = 0xf0452fbb07a47cf5, +} diff --git a/flake.lock b/flake.lock new file mode 100644 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1783776592, + "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 --- /dev/null +++ b/flake.nix @@ -0,0 +1,23 @@ +{ + description = "cdl"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + }; + + outputs = + { self, nixpkgs }: + let + lib = nixpkgs.lib; + forAllSystems = f: lib.genAttrs lib.systems.flakeExposed (s: f nixpkgs.legacyPackages.${s}); + in + { + packages = forAllSystems (pkgs: { + default = pkgs.callPackage ./nix/package.nix { }; + }); + devShells = forAllSystems (pkgs: { + default = pkgs.callPackage ./nix/shell.nix { }; + }); + formatter = forAllSystems (pkgs: pkgs.callPackage ./nix/formatter.nix { }); + }; +} diff --git a/meow.sh b/meow.sh new file mode 100644 --- /dev/null +++ b/meow.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +len=4 + +for text in 'meow' 'rawr' 'nya' ':3'; do + printf '%s\033[%bG' "$text" $((len + 2)) + echo ':3' +done diff --git a/readme.md b/readme.md new file mode 100644 --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +cute fetch client written in zig :3 + +pronounced like "cuddle" diff --git a/nix/deps.nix b/nix/deps.nix new file mode 100644 --- /dev/null +++ b/nix/deps.nix @@ -0,0 +1,4 @@ +{ + linkFarm, +}: +linkFarm "zig-packages" [ ] diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,34 @@ +{ + lib, + stdenv, + callPackage, + + zig, +}: +stdenv.mkDerivation { + pname = "cdl"; + version = "0.1.0"; + src = ../.; + + nativeBuildInputs = [ zig ]; + buildInputs = [ ]; + + doCheck = false; + checkPhase = '' + runHook preCheck + zig build test + runHook postCheck + ''; + + zigBuildFlags = [ + "--system" + "${callPackage ./deps.nix { }}" + ]; + + meta = { + homepage = "https://codeberg.org/comfysage/cdl"; + license = lib.licenses.asl20; + platforms = [ "x86_64-linux" ]; + mainProgram = "maivi"; + }; +} diff --git a/nix/shell.nix b/nix/shell.nix new file mode 100644 --- /dev/null +++ b/nix/shell.nix @@ -0,0 +1,14 @@ +{ + zig, + just, + watchexec, + + mkShellNoCC, +}: +mkShellNoCC { + packages = [ + zig + just + watchexec + ]; +} diff --git a/src/ascii.txt b/src/ascii.txt new file mode 100644 --- /dev/null +++ b/src/ascii.txt @@ -0,0 +1,4 @@ + へ ♡ ╱|、 + ૮ - ՛) (` - 7 + / ⁻ ៸| |、⁻〵 + 乀(ˍ, ل ل じしˍ,)ノ diff --git a/src/cmd.zig b/src/cmd.zig new file mode 100644 --- /dev/null +++ b/src/cmd.zig @@ -0,0 +1,46 @@ +const std = @import("std"); + +const cdl = @import("root.zig"); + +pub const Options = struct { + trimmed: bool = false, +}; + +pub fn spawn(argv: []const []const u8, opts: ?Options) ![]const u8 { + if (!std.process.can_spawn) { + return error.NoChildProcess; + } + + var child = try std.process.spawn(cdl.io, .{ + .argv = argv, + .environ_map = cdl.environ_map, + .stdin = .ignore, + .stdout = .pipe, + .stderr = .ignore, + }); + + var stdout_reader = child.stdout.?.readerStreaming(cdl.io, &.{}); + const stdout = stdout_reader.interface.allocRemaining(cdl.gpa, .limited(0x100)) catch return error.ReadFailure; + errdefer cdl.gpa.free(stdout); + + const term = try child.wait(cdl.io); + + const raw = blk: switch (term) { + .exited => |code| { + if (code != 0) { + return error.ExitCodeFailure; + } + break :blk stdout; + }, + .signal, .stopped => return error.ProcessTerminated, + .unknown => return error.ProcessTerminated, + }; + if (opts == null) return raw; + + if (opts) |o| if (o.trimmed) { + defer cdl.gpa.free(raw); + return try cdl.gpa.dupe(u8, std.mem.trim(u8, raw, " \n\r")); + }; + + return raw; +} diff --git a/src/info.zig b/src/info.zig new file mode 100644 --- /dev/null +++ b/src/info.zig @@ -0,0 +1,123 @@ +const std = @import("std"); + +const cdl = @import("root.zig"); + +/// owned +pub fn hostname() ![]const u8 { + var buf: [std.posix.HOST_NAME_MAX]u8 = undefined; + const hostname_buf = try std.posix.gethostname(&buf); + return try cdl.gpa.dupe(u8, hostname_buf); +} + +pub fn user() !?[]const u8 { + const pwinfo = try PWInfo.get(); + if (pwinfo) |pw| return pw.user; + + if (cdl.environ_map) |envmap| return envmap.get("USER"); + if (cdl.environ_map) |envmap| return envmap.get("LOGNAME"); + + return null; +} + +pub fn shell() !?[]const u8 { + const shell_str = blk: { + const pwinfo = try PWInfo.get(); + if (pwinfo) |pw| break :blk pw.shell; + + break :blk if (cdl.environ_map) |envmap| + envmap.get("SHELL") + else + null; + } orelse return null; + + if (std.mem.cutScalarLast(u8, shell_str, '/')) |parts| + return parts[1]; + + return shell_str; +} + +pub fn osname() !?[]const u8 { + const f = try std.Io.Dir.openFileAbsolute(cdl.io, "/etc/os-release", .{}); + + var buf: [0x1000]u8 = undefined; + var file_reader = f.reader(cdl.io, &buf); + var reader = &file_reader.interface; + + while (try reader.takeDelimiter('\n')) |line| + if (std.mem.cutPrefix(u8, line, "NAME=")) |name| + return try cdl.allocator.dupe(u8, trimquotes(name)); + return null; +} + +const PWInfo = struct { + user: ?[]const u8, + shell: ?[]const u8, + dir: ?[]const u8, + + var cached: ?@This() = null; + + pub fn get() !?PWInfo { + if (PWInfo.cached) |c| return c; + + const f = try std.Io.Dir.openFileAbsolute(cdl.io, "/etc/passwd", .{}); + + var buf: [0x1000]u8 = undefined; + var file_reader = f.reader(cdl.io, &buf); + var reader = &file_reader.interface; + + const uid = std.os.linux.getuid(); + w: while (try reader.takeDelimiter('\n')) |line| { + var parts = std.mem.splitScalar(u8, line, ':'); + var pw_user: ?[]const u8 = null; + var pw_shell: ?[]const u8 = null; + var pw_dir: ?[]const u8 = null; + + var i: usize = 0; + while (parts.next()) |part| : (i = i + 1) switch (i) { + 0 => pw_user = part, + 2 => if (try std.fmt.parseInt(u32, part, 10) != uid) continue :w, + 5 => pw_dir = part, + 6 => pw_shell = part, + else => {}, + }; + + return .{ + .user = if (pw_user) |user_| try cdl.allocator.dupe(u8, user_) else null, + .shell = if (pw_shell) |shell_| try cdl.allocator.dupe(u8, shell_) else null, + .dir = if (pw_dir) |dir_| try cdl.allocator.dupe(u8, dir_) else null, + }; + } + + return null; + } +}; + +// helpers ==================================================================== + +fn trimquotes(s: []const u8) []const u8 { + if (s.len < 2) return s; + const open = switch (s[0]) { + '"' => s[0], + '\'' => s[0], + else => return s, + }; + // assume malformed + if (s[s.len - 1] != open) return s[1..]; + return s[1 .. s.len - 1]; +} + +test "trim quotes" { + try std.testing.expectEqualStrings("meow", trimquotes("meow")); + try std.testing.expectEqualStrings("meow", trimquotes( + \\"meow" + )); + try std.testing.expectEqualStrings("meow", trimquotes( + \\'meow' + )); + try std.testing.expectEqualStrings("meow", trimquotes( + \\'meow + )); + try std.testing.expectEqualStrings("meow", trimquotes( + \\"meow + )); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,108 @@ +const std = @import("std"); + +const cdl = @import("cdl"); + +const ascii = @embedFile("ascii.txt"); +const ascii_lines = blk: { + var ar: []const []const u8 = &.{}; + var it = std.mem.splitScalar(u8, ascii, '\n'); + while (it.next()) |line| if (line.len != 0) { + ar = ar ++ .{line}; + }; + break :blk ar; +}; + +const base_padding = 1; + +// へ ♡ ╱|、 SYS 󰧟 bmo +// ૮ - ՛) (` - 7 ├  󰧟 NixOS 26.11 (Zokor) x86_64 +// / ⁻ ៸| |、⁻〵 └  󰧟 zsh +// 乀(ˍ, ل ل じしˍ,)ノ WM 󰧟 niri (Wayland) +// ├  󰧟 ghostty 1.3.1 +// └  󰧟 nvim +// THEME 󰧟 evergarden +// └  󰧟 Fantasque Sans Mono (14pt) [GTK2/3/4] +// ● ● ● ● ● ● ● ● + +pub fn main(init: std.process.Init) !void { + cdl.init(init); + defer cdl.deinit(); + + const stdout = std.Io.File.stdout(); + var buf: [0x400]u8 = undefined; + var file_writer = stdout.writer(cdl.io, &buf); + try term.init(&file_writer.interface); + + // SYS ==================================================================== + + const hostname = try cdl.info.hostname(); + defer cdl.gpa.free(hostname); + + const user = try cdl.info.user(); + const shell = try cdl.info.shell(); + + const osname = try cdl.info.osname(); + + try term.writefmt( + \\sys {s} + , .{hostname}); + if (user) |v| + try term.writefmt( + \\ ├ 󰋞 {s} + , .{v}); + if (osname) |v| + try term.writefmt( + \\ ├  {s} + , .{v}); + if (shell) |v| + try term.writefmt( + \\ └  {s} + , .{v}); + + try term.writer.flush(); +} + +const term = struct { + pub var writer: *std.Io.Writer = undefined; + + var ascii_line_widths: [ascii_lines.len]usize = undefined; + var longest: usize = undefined; + + pub fn init(w: *std.Io.Writer) !void { + writer = w; + + _ = std.c.setlocale(.ALL, ""); + + for (ascii_lines, 0..) |line, c| { + const len = try cdl.strwidth.width(line); + ascii_line_widths[c] = len; + } + + longest = blk: { + var max: usize = 0; + for (ascii_line_widths) |len| if (len > max) { + max = len; + }; + break :blk max; + }; + } + + pub fn writefmt(comptime format: []const u8, args: anytype) !void { + try writeascii(); + + const buf = try std.fmt.allocPrint(cdl.gpa, format, args); + defer cdl.gpa.free(buf); + + try writer.writeAll(buf); + try writer.writeByte('\n'); + try writer.flush(); + } + + var i: usize = 0; + pub fn writeascii() !void { + defer i += 1; + try writer.writeAll(ascii_lines[i]); + const len = ascii_line_widths[i]; + for (0..longest - len + base_padding) |_| try writer.writeByte(' '); + } +}; diff --git a/src/root.zig b/src/root.zig new file mode 100644 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,25 @@ +const std = @import("std"); + +pub const cmd = @import("cmd.zig"); +pub const info = @import("info.zig"); +pub const strwidth = @import("strwidth.zig"); + +pub var io: std.Io = undefined; +pub var gpa: std.mem.Allocator = undefined; +pub var environ_map: ?*std.process.Environ.Map = null; + +var arena: std.heap.ArenaAllocator = undefined; +pub var allocator: std.mem.Allocator = undefined; + +pub fn init(p_init: std.process.Init) void { + io = p_init.io; + gpa = p_init.gpa; + environ_map = p_init.environ_map; + + arena = .init(gpa); + allocator = arena.allocator(); +} + +pub fn deinit() void { + arena.deinit(); +} diff --git a/src/strwidth.zig b/src/strwidth.zig new file mode 100644 --- /dev/null +++ b/src/strwidth.zig @@ -0,0 +1,40 @@ +// Copyright june 2026 + +const std = @import("std"); +const unicode = std.unicode; +const mem = std.mem; +const c = std.c; + +const wc = @import("wchar"); + +pub fn width(s: []const u8) !usize { + var w: usize = 0; + var i: usize = 0; + + while (i < s.len) { + const len = try unicode.utf8ByteSequenceLength(s[i]); + const dec = try unicode.utf8Decode(s[i .. i + len]); + + const cw = wc.wcwidth(@intCast(dec)); + if (cw > 0) w += @intCast(cw); + + i += len; + } + + return w; +} + +test "strwidth" { + try std.testing.expectEqual(16, width( + \\ へ ♡ ╱|、 + )); + try std.testing.expectEqual(17, width( + \\ ૮ - ՛) (` - 7 + )); + try std.testing.expectEqual(18, width( + \\ / ⁻ ៸| |、⁻〵 + )); + try std.testing.expectEqual(22, width( + \\ 乀(ˍ, ل ل じしˍ,)ノ + )); +} diff --git a/src/wchar.h b/src/wchar.h new file mode 100644 --- /dev/null +++ b/src/wchar.h @@ -0,0 +1,2 @@ +#define _XOPEN_SOURCE 700 +#include // mbrtowc, wcwidth -- tangled.sh