Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ deps/notcurses
.idea
.clj-kondo
.nrepl-port
.zig-cache
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ git clone https://github.com/dankamongmen/notcurses.git deps/notcurses
cd deps/notcurses
mkdir build && cd build
cmake -DUSE_MULTIMEDIA=none -DUSE_PANDOC=OFF ..

# OLD COMMENT
# We just need `cmake` to generate some headers, no need to actually `make` since rest will be handled by Zig
# In case of errors, try `git checkout v3.0.9` and re-run cmake as I tested it with this version.

# 0.15 UPDATE: The current build script requires make to built libncurses-core
make
Copy link

@EngineersBox EngineersBox Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't exactly true, if we're required to have an externally built static lib then sure, but since we are compiling the code into a zig module this is not necessary.

```

### Build and run
Expand Down
142 changes: 77 additions & 65 deletions build.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const Builder = @import("std").build.Builder;
const std = @import("std");

pub fn build(b: *Builder) void {
pub fn build(b: *std.Build) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
Expand All @@ -14,85 +14,97 @@ pub fn build(b: *Builder) void {

const notcurses_source_path = "deps/notcurses";

const notcurses = b.addStaticLibrary(.{
.name = "notcurses",
const notcurses_module = b.createModule(.{
.target = target,
.optimize = optimize,
// notcurses has saome undefined benavior which makes the demo crash with
// illegal instruction, disabling UBSAN to make it work (-fno-sanitize-c)
.sanitize_c = std.zig.SanitizeC.off,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can add .link_libc = true, and remove the deprecated notcurses.linkLibC() usage below

});
const notcurses = b.addLibrary(.{
.name = "notcurses",
.root_module = notcurses_module,
});
// notcurses has saome undefined benavior which makes the demo crash with
// illegal instruction, disabling UBSAN to make it work (-fno-sanitize-c)
notcurses.disable_sanitize_c = true;

// TODO: Reenable this
//notcurses.disable_sanitize_c = true;
notcurses.linkLibC();

notcurses.linkSystemLibrary("deflate");
notcurses.linkSystemLibrary("ncurses");
notcurses.linkSystemLibrary("readline");
notcurses.linkSystemLibrary("unistring");
notcurses.linkSystemLibrary("z");
notcurses.addIncludePath(b.path(notcurses_source_path ++ "/include"));
notcurses.addIncludePath(b.path(notcurses_source_path ++ "/build/include"));
notcurses.addIncludePath(b.path(notcurses_source_path ++ "/src"));
notcurses.addCSourceFiles(.{
.files = &[_][]const u8{
notcurses_source_path ++ "/src/compat/compat.c",

notcurses.addIncludePath(.{ .path = notcurses_source_path ++ "/include" });
notcurses.addIncludePath(.{ .path = notcurses_source_path ++ "/build/include" });
notcurses.addIncludePath(.{ .path = notcurses_source_path ++ "/src" });
notcurses.addCSourceFiles(&[_][]const u8{
notcurses_source_path ++ "/src/compat/compat.c",
notcurses_source_path ++ "/src/lib/automaton.c",
notcurses_source_path ++ "/src/lib/banner.c",
notcurses_source_path ++ "/src/lib/blit.c",
notcurses_source_path ++ "/src/lib/debug.c",
notcurses_source_path ++ "/src/lib/direct.c",
notcurses_source_path ++ "/src/lib/fade.c",
notcurses_source_path ++ "/src/lib/fd.c",
notcurses_source_path ++ "/src/lib/fill.c",
notcurses_source_path ++ "/src/lib/gpm.c",
notcurses_source_path ++ "/src/lib/in.c",
notcurses_source_path ++ "/src/lib/kitty.c",
notcurses_source_path ++ "/src/lib/layout.c",
notcurses_source_path ++ "/src/lib/linux.c",
notcurses_source_path ++ "/src/lib/menu.c",
notcurses_source_path ++ "/src/lib/metric.c",
notcurses_source_path ++ "/src/lib/mice.c",
notcurses_source_path ++ "/src/lib/notcurses.c",
notcurses_source_path ++ "/src/lib/plot.c",
notcurses_source_path ++ "/src/lib/progbar.c",
notcurses_source_path ++ "/src/lib/reader.c",
notcurses_source_path ++ "/src/lib/reel.c",
notcurses_source_path ++ "/src/lib/render.c",
notcurses_source_path ++ "/src/lib/selector.c",
notcurses_source_path ++ "/src/lib/sixel.c",
notcurses_source_path ++ "/src/lib/sprite.c",
notcurses_source_path ++ "/src/lib/stats.c",
notcurses_source_path ++ "/src/lib/tabbed.c",
notcurses_source_path ++ "/src/lib/termdesc.c",
notcurses_source_path ++ "/src/lib/tree.c",
notcurses_source_path ++ "/src/lib/unixsig.c",
notcurses_source_path ++ "/src/lib/util.c",
notcurses_source_path ++ "/src/lib/visual.c",
notcurses_source_path ++ "/src/lib/windows.c",
},
.flags = &[_][]const u8{
"-std=gnu11",
"-D_GNU_SOURCE", // to make memory management work, see sys/mman.h
"-DUSE_MULTIMEDIA=none",
"-DUSE_QRCODEGEN=OFF",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll want to drop this define, since the C files make use of it with #ifdef without a value check.

"-DPOLLRDHUP=0x2000",
},
});

notcurses_source_path ++ "/src/lib/automaton.c",
notcurses_source_path ++ "/src/lib/banner.c",
notcurses_source_path ++ "/src/lib/blit.c",
notcurses_source_path ++ "/src/lib/debug.c",
notcurses_source_path ++ "/src/lib/direct.c",
notcurses_source_path ++ "/src/lib/fade.c",
notcurses_source_path ++ "/src/lib/fd.c",
notcurses_source_path ++ "/src/lib/fill.c",
notcurses_source_path ++ "/src/lib/gpm.c",
notcurses_source_path ++ "/src/lib/in.c",
notcurses_source_path ++ "/src/lib/kitty.c",
notcurses_source_path ++ "/src/lib/layout.c",
notcurses_source_path ++ "/src/lib/linux.c",
notcurses_source_path ++ "/src/lib/menu.c",
notcurses_source_path ++ "/src/lib/metric.c",
notcurses_source_path ++ "/src/lib/mice.c",
notcurses_source_path ++ "/src/lib/notcurses.c",
notcurses_source_path ++ "/src/lib/plot.c",
notcurses_source_path ++ "/src/lib/progbar.c",
notcurses_source_path ++ "/src/lib/reader.c",
notcurses_source_path ++ "/src/lib/reel.c",
notcurses_source_path ++ "/src/lib/render.c",
notcurses_source_path ++ "/src/lib/selector.c",
notcurses_source_path ++ "/src/lib/sixel.c",
notcurses_source_path ++ "/src/lib/sprite.c",
notcurses_source_path ++ "/src/lib/stats.c",
notcurses_source_path ++ "/src/lib/tabbed.c",
notcurses_source_path ++ "/src/lib/termdesc.c",
notcurses_source_path ++ "/src/lib/tree.c",
notcurses_source_path ++ "/src/lib/unixsig.c",
notcurses_source_path ++ "/src/lib/util.c",
notcurses_source_path ++ "/src/lib/visual.c",
notcurses_source_path ++ "/src/lib/windows.c",
}, &[_][]const u8{
"-std=gnu11",
"-D_GNU_SOURCE", // to make memory management work, see sys/mman.h
"-DUSE_MULTIMEDIA=none",
"-DUSE_QRCODEGEN=OFF",
"-DPOLLRDHUP=0x2000",
const exe_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe_module.linkLibrary(notcurses);
exe_module.linkSystemLibrary("qrcodegen", .{});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop this link as well if the previously mentioned define is removed

exe_module.linkSystemLibrary("deflate", .{});
exe_module.linkSystemLibrary("tinfo", .{});
exe_module.linkSystemLibrary("ncurses", .{});
exe_module.linkSystemLibrary("readline", .{});
exe_module.linkSystemLibrary("unistring", .{});
exe_module.linkSystemLibrary("z", .{});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be attached to the notcurses_module instead of exe_module


const exe = b.addExecutable(.{
.name = "demo",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
.root_module = exe_module,
});
b.installArtifact(exe);
exe.linkLibC();

// exe.linkSystemLibrary("notcurses-core");
// exe.addObjectFile(notcurses_source_path ++ "/build/libnotcurses-core.a");

exe.addIncludePath(.{ .path = notcurses_source_path ++ "/include" });
exe.linkLibrary(notcurses);
exe.linkSystemLibrary("notcurses-core");
exe.addObjectFile(b.path(notcurses_source_path ++ "/build/libnotcurses-core.a"));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of seem to be needed as we are compiling the code into a Zig module, not building it externally into a static lib.


exe.linkSystemLibrary("qrcodegen");
exe.addIncludePath(b.path(notcurses_source_path ++ "/include"));

const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
Expand Down
64 changes: 32 additions & 32 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ fn linear_transition(start: anytype, end: anytype, duration: u64, diff: u64) @Ty

fn transition_rgb(start: u32, end: u32, duration: u64, diff: u64) u32 {
var rgb: u32 = 0;
var r = linear_transition(@as(c_int, @intCast(nc.ncchannel_r(start))), @as(c_int, @intCast(nc.ncchannel_r(end))), duration, diff);
var g = linear_transition(@as(c_int, @intCast(nc.ncchannel_g(start))), @as(c_int, @intCast(nc.ncchannel_g(end))), duration, diff);
var b = linear_transition(@as(c_int, @intCast(nc.ncchannel_b(start))), @as(c_int, @intCast(nc.ncchannel_b(end))), duration, diff);
const r = linear_transition(@as(c_int, @intCast(nc.ncchannel_r(start))), @as(c_int, @intCast(nc.ncchannel_r(end))), duration, diff);
const g = linear_transition(@as(c_int, @intCast(nc.ncchannel_g(start))), @as(c_int, @intCast(nc.ncchannel_g(end))), duration, diff);
const b = linear_transition(@as(c_int, @intCast(nc.ncchannel_b(start))), @as(c_int, @intCast(nc.ncchannel_b(end))), duration, diff);
nc.ncchannel_set_rgb8_clipped(&rgb, r, g, b);
return rgb;
}
Expand All @@ -37,8 +37,8 @@ fn make_boxes_start(dimy: anytype, dimx: anytype) [BOX_NUM][4]c_int {
{
var i: usize = 0;
while (i < bs.len) : (i += 1) {
var y: c_int = -1;
var x: c_int = @divTrunc(@as(c_int, @intCast(dimx)), 2);
const y: c_int = -1;
const x: c_int = @divTrunc(@as(c_int, @intCast(dimx)), 2);
bs[i][0] = y;
bs[i][1] = x;
bs[i][2] = y + 2;
Expand All @@ -53,8 +53,8 @@ fn make_boxes_bottom_out(dimy: anytype, dimx: anytype) [BOX_NUM][4]c_int {
{
var i: usize = 0;
while (i < bs.len) : (i += 1) {
var y: c_int = (@as(c_int, @intCast(dimy)) + 4);
var x: c_int = @divTrunc(@as(c_int, @intCast(dimx)), 2);
const y: c_int = (@as(c_int, @intCast(dimy)) + 4);
const x: c_int = @divTrunc(@as(c_int, @intCast(dimx)), 2);
bs[i][0] = y;
bs[i][1] = x;
bs[i][2] = y + 2;
Expand All @@ -65,29 +65,29 @@ fn make_boxes_bottom_out(dimy: anytype, dimx: anytype) [BOX_NUM][4]c_int {
}

fn make_boxes_arranged(dim_y: anytype, dim_x: anytype) [BOX_NUM][4]c_int {
var dimx = @as(c_int, @intCast(dim_x));
var dimy = @as(c_int, @intCast(dim_y));
var x0: c_int = 2;
var x1 = @divFloor(dimx * 40, 100);
var x2 = @divFloor(dimx * 55, 100);
var x3 = @divFloor(dimx * 85, 100);
var x4 = dimx;
var y0: c_int = 1;
var y1 = @divFloor(dimy * 18, 100);
var y2 = @divFloor(dimy * 22, 100);
var y3 = @divFloor(dimy * 35, 100);
var y4 = @divFloor(dimy * 55, 100);
var y5 = @divFloor(dimy * 70, 100);
var y6 = dimy;
var bs = [BOX_NUM][4]c_int{ .{ y0, x0, y5, x1 }, .{ y5, x0, y6, x1 }, .{ y0, x1, y2, x2 }, .{ y2, x1, y5, x2 }, .{ y5, x1, y6, x2 }, .{ y0, x2, y3, x3 }, .{ y3, x2, y4, x3 }, .{ y4, x2, y6, x4 }, .{ y0, x3, y1, x4 }, .{ y1, x3, y4, x4 } };
const dimx = @as(c_int, @intCast(dim_x));
const dimy = @as(c_int, @intCast(dim_y));
const x0: c_int = 2;
const x1 = @divFloor(dimx * 40, 100);
const x2 = @divFloor(dimx * 55, 100);
const x3 = @divFloor(dimx * 85, 100);
const x4 = dimx;
const y0: c_int = 1;
const y1 = @divFloor(dimy * 18, 100);
const y2 = @divFloor(dimy * 22, 100);
const y3 = @divFloor(dimy * 35, 100);
const y4 = @divFloor(dimy * 55, 100);
const y5 = @divFloor(dimy * 70, 100);
const y6 = dimy;
const bs = [BOX_NUM][4]c_int{ .{ y0, x0, y5, x1 }, .{ y5, x0, y6, x1 }, .{ y0, x1, y2, x2 }, .{ y2, x1, y5, x2 }, .{ y5, x1, y6, x2 }, .{ y0, x2, y3, x3 }, .{ y3, x2, y4, x3 }, .{ y4, x2, y6, x4 }, .{ y0, x3, y1, x4 }, .{ y1, x3, y4, x4 } };
return bs;
}

fn make_boxes_grid(dimy: anytype, dimx: anytype) [BOX_NUM][4]c_int {
const boxh: c_int = @divTrunc(@as(c_int, @intCast(dimy)), 5);
const boxw: c_int = (boxh * 2);
var y0: c_int = @divFloor(@as(c_int, @intCast(dimy)) * 20, 100);
var x0: c_int = @divFloor(@as(c_int, @intCast(dimx)) * 20, 100);
const y0: c_int = @divFloor(@as(c_int, @intCast(dimy)) * 20, 100);
const x0: c_int = @divFloor(@as(c_int, @intCast(dimx)) * 20, 100);
var bs: [BOX_NUM][4]c_int = undefined;
{
var i: usize = 0;
Expand Down Expand Up @@ -158,7 +158,7 @@ fn draw_boxes_bordered(planes: [BOX_NUM]*nc.ncplane) !void {
{
var i: usize = 0;
while (i < planes.len) : (i += 1) {
var plane = planes[i];
const plane = planes[i];
nc.ncplane_erase(plane);
try nc.err(nc.ncplane_cursor_move_yx(plane, 0, 0));
_ = nc.ncplane_rounded_box(plane, 0, 0, nc.ncplane_dim_y(plane) - 1, nc.ncplane_dim_x(plane) - 1, 0);
Expand Down Expand Up @@ -218,7 +218,7 @@ const PositionContext = struct {
to: c_int,
};
fn run_transition(ncs: *nc.notcurses, duration: u64, ctx: anytype, render: fn (@TypeOf(ctx), u64, u64) nc.Error!void) !void {
var time_start: u64 = time.get_time_ns();
const time_start: u64 = time.get_time_ns();
var t: u64 = time_start;
while (t < (time_start + duration)) : (t = time.get_time_ns()) {
try render(ctx, t - time_start, duration);
Expand All @@ -240,11 +240,11 @@ fn run_serial_transition(ncs: *nc.notcurses, duration: u64, comptime render: fn

pub fn main() !void {
var nc_opts: nc.notcurses_options = nc.default_notcurses_options;
var ncs: *nc.notcurses = (nc.notcurses_core_init(&nc_opts, null) orelse @panic("notcurses_core_init() failed"));
const ncs: *nc.notcurses = (nc.notcurses_core_init(&nc_opts, null) orelse @panic("notcurses_core_init() failed"));
defer _ = nc.notcurses_stop(ncs);
var dimy: c_uint = undefined;
var dimx: c_uint = undefined;
var n: *nc.ncplane = (nc.notcurses_stddim_yx(ncs, &dimy, &dimx) orelse unreachable);
const n: *nc.ncplane = (nc.notcurses_stddim_yx(ncs, &dimy, &dimx) orelse unreachable);
dimx = @max(dimx, 80);
dimy = @max(dimy, 25);
var std_chan: u64 = 0;
Expand Down Expand Up @@ -315,14 +315,14 @@ pub fn main() !void {
outer: {
var loop: usize = 0;
while (true) : (loop += 1) {
var duration: u64 = 1.0E9;
var time_start: u64 = time.get_time_ns();
const duration: u64 = 1.0E9;
const time_start: u64 = time.get_time_ns();
var t: u64 = time_start;
while (t < (time_start + duration)) : (t = time.get_time_ns()) {
{
var i: usize = 0;
while (i < box_planes.len) : (i += 1) {
var plane = box_planes[i];
const plane = box_planes[i];
const colors = [4]u32{ box_colors[i], 16777215, box_colors[i], 0 };
var corners: [4]u32 = undefined;
{
Expand All @@ -336,7 +336,7 @@ pub fn main() !void {
}
try nc.err(nc.notcurses_render(ncs));
time.sleep_until_ns(t + step_ns);
var keypress: c_uint = nc.notcurses_get_nblock(ncs, null);
const keypress: c_uint = nc.notcurses_get_nblock(ncs, null);
if (keypress == 'q') {
break :outer;
}
Expand Down
45 changes: 44 additions & 1 deletion src/notcurses.zig
Original file line number Diff line number Diff line change
@@ -1,7 +1,50 @@
const c = @cImport({
@cInclude("notcurses/notcurses.h");
});
pub usingnamespace c;

// Re-export the C symbols we need
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the re-export necessary now?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not have requisite experience or knowledge to answer this question

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm currently taking the baton here for making this work on 0.15 (and 0.16 to release soon).

This does not need to be re-exported. While usingnamespace was essentially removed entirely, we can just directly re-export via an alias:

pub const nc = @cImport({
    @cInclude("notcurses/notcurses.h");
});

Then using the lib is just a matter of:

const nc = @import("notcurses.zig").nc;

When 0.16 or 0.17 rolls around (depending on the issue status), @cImport will disappear in favour of direct declartion in the build script as a zig module instead (ziglang/zig#20630). So, that will essentially allow direct import like @import("notcurses") or something similar.

pub const notcurses = c.notcurses;
pub const ncplane = c.ncplane;
pub const notcurses_options = c.notcurses_options;
pub const ncplane_options = c.ncplane_options;
pub const ncselector_options = c.ncselector_options;

// Re-export functions
pub const notcurses_core_init = c.notcurses_core_init;
pub const notcurses_stop = c.notcurses_stop;
pub const notcurses_stddim_yx = c.notcurses_stddim_yx;
pub const notcurses_render = c.notcurses_render;
pub const notcurses_get_nblock = c.notcurses_get_nblock;

pub const ncplane_create = c.ncplane_create;
pub const ncplane_set_base = c.ncplane_set_base;
pub const ncplane_erase = c.ncplane_erase;
pub const ncplane_move_yx = c.ncplane_move_yx;
pub const ncplane_resize_simple = c.ncplane_resize_simple;
pub const ncplane_gradient2x1 = c.ncplane_gradient2x1;
pub const ncplane_rounded_box = c.ncplane_rounded_box;
pub const ncplane_cursor_move_yx = c.ncplane_cursor_move_yx;
pub const ncplane_putstr_yx = c.ncplane_putstr_yx;
pub const ncplane_dim_y = c.ncplane_dim_y;
pub const ncplane_dim_x = c.ncplane_dim_x;
pub const ncplane_x = c.ncplane_x;
pub const ncplane_y = c.ncplane_y;

pub const ncchannels_set_bg_rgb = c.ncchannels_set_bg_rgb;
pub const ncchannels_set_bg_alpha = c.ncchannels_set_bg_alpha;
pub const ncchannels_set_fg_rgb = c.ncchannels_set_fg_rgb;
pub const ncchannels_set_bchannel = c.ncchannels_set_bchannel;
pub const ncchannels_set_fchannel = c.ncchannels_set_fchannel;

pub const ncchannel_r = c.ncchannel_r;
pub const ncchannel_g = c.ncchannel_g;
pub const ncchannel_b = c.ncchannel_b;
pub const ncchannel_set_rgb8_clipped = c.ncchannel_set_rgb8_clipped;

// Re-export constants
pub const NCLOGLEVEL_SILENT = c.NCLOGLEVEL_SILENT;
pub const NCALPHA_BLEND = c.NCALPHA_BLEND;
pub const NC_BGDEFAULT_MASK = c.NC_BGDEFAULT_MASK;
pub const default_notcurses_options = c.notcurses_options{
.termtype = null,
.loglevel = c.NCLOGLEVEL_SILENT,
Expand Down