Simple Menu Navigation and selection

Hi all, my first post, be gentle.
I want to print on terminal (linux, if relevant) starting at col x, row y a menu:
a
b
c
Navigate that menu with Up/Down arrow keys and upon selecting with Enter, print “You selected {}”, selection.

My zig version: 0.15.1

PS

  1. I’m aware this will require entering raw mode in terminal but more than that is beyond me and so is understanding the documentation of Libvaxis: A modern TUI library to understand if that is really what I need.

  2. It took me just 8 months to exit VIM and recently I managed to write Hello Rust but that was AI assisted, LLMs aren’t yet very good with ZIG (unfortunately for novices like me) so… as I said first, be gentle ;))

With looking for “TUIs” you’re generally on the right track, you can also read up on “curses” and “ncurses” which are ancient TUI frameworks. Under the hood it’s all terminal protocol escape sequences which are tricky/ugly to do manually, but aTUI framework generally handles these details for you.

5 Likes

Thanks @flooh
I managed with Termion a full 4 hand diagram (I’m doing the probably classic “learn codding by doing a bridge game”) and I was about to switch to Crossterm or maybe even Cursive or Ratatui but I had this simple a,b,c example as a starting point about how to import the library and setup the simplest interaction. Without that, the syntax and the concepts involved are way above my head for now…

Welcome to Ziggit!

You might want to have another look at vaxis, there‘s an example in the repo which sounds close to what you want:

you can run it like so:

zig build example -Dexample=cli

I‘m currently using vaxis for a project and I find it very pleasant to work with once you get the hang of it.

2 Likes

Later edit:
Thanks all, here it is what I did:

I put the example linked by Justus (thanks) in main.zig

I did zig fetch as instructed here: GitHub - rockorager/libvaxis: a modern tui library written in zig

elbci@localhost:~/Documents/ZIG/BridgeSeek> zig fetch --save git+https://github.com/rockorager/libvaxis.git
info: resolved to commit 95b167909f8aaec7f9b2315e6a1f64d9a48afe9b

I replaced in my build.zig (autogenerated by zig init)

     const exe = b.addExecutable(.{
         .name = "BridgeSeek",
         .root_module = b.createModule(.{
             // b.createModule defines a new module just like b.addModule but,
             // unlike b.addModule, it does not expose the module to consumers of
             // this package, which is why in this case we don't have to give it a name.
             .root_source_file = b.path("src/main.zig"),
             // Target and optimization levels must be explicitly wired in when
             // defining an executable or library (in the root module), and you
             // can also hardcode a specific target for an executable or library
             // definition if desireable (e.g. firmware for embedded devices).
             .target = target,
             .optimize = optimize,
             // List of modules available for import in source files part of the
             // root module.
             .imports = &.{
                 // Here "BridgeSeek" is the name you will use in your source code to
                 // import this module (e.g. `@import("BridgeSeek")`). The name is
                 // repeated because you are allowed to rename your imports, which
                 // can be extremely useful in case of collisions (which can happen
                 // importing modules from different packages).
                 .{ .name = "BridgeSeek", .module = mod },
             },
         }),
     });

with

    // // NEW CODE //////////////////////////////////
    // Create a module for the executable (for ZLS support)
    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Add vaxis dependency to the module
    const vaxis = b.dependency("vaxis", .{
        .target = target,
        .optimize = optimize,
    });
    exe_mod.addImport("vaxis", vaxis.module("vaxis"));

    // Create executable using this module
    const exe = b.addExecutable(.{
        .name = "BridgeSeek",
        .root_module = exe_mod,
    });

    // // END NEW CODE /////////////////////////////////////////////

Now the code compiles and runs with zig build run.

If the comments are too much visual noise, you can remove them.
For future reference, zig init --minimal or -m will create a minimal project.
Probably more minimal than you expect, so I’d recommend against it until you are confident with the build system.

For the most part, you can copy and paste what vaxis tells you, excluding things you already have.

The snippet for zls support is AFAIK unnecessary, the only thing zls needs (only necessary if you want it to report compile errors, other errors will show regardless) is a step called check that depends on everything you want it to check. see Build-On-Save - zigtools for more information.

1 Like

@vulpesx
Thanks, I bet the comments in build.zig contain usefull info I’m just not ready to absorb yet.
I just needed to know that it was a matter of replacing const exe, not just adding the new lines.

Now,
@Justus2308 (or whomever of the 99.9% smarter than me ppl here):

The example compiles and runs, terminal enters raw mode.

but the result is just:

it changes on Tab:

on ArrowKeys:


and exits clean on Ctrl+C restoring the terminal proper.

To get to my goal I need a clean menu positioned at a custom col, row on the terminal.
PS I’m on a vanilla Tumbleweed, KDE Plasma, Konsole terminal emulator.

normal printing/logging will have issues as its fighting vaxis.

the easy solution is to redirect stderr, logging(by default) and debug.print print to stderr, via [cmd] 2> err.txt you can then look at err.txt for the output.

The more robust and flexable option is to override the log function to output somewhere else:

// the root file of your root module
pub const std_options: std.Options = .{
    .logFn = myLogFn,
};
fn myLogFn(
        comptime message_level: log.Level,
        comptime scope: @TypeOf(.enum_literal),
        comptime format: []const u8,
        args: anytype,
    ) void {
    // do your logging
}
4 Likes

Inspired by @vulpesx I just comment out
// log.err(“enter”, .{});
// log.err(“i = {d}, j = {d}, opt = {s}”, .{ i, j, opt });
at lines 69 and 92 in the example from @Justus2308 : libvaxis/examples/cli.zig at main · rockorager/libvaxis · GitHub

Great progress, the menu appears after Tab, advances to next option on Tab and finally Enter displays the selection. I guess I can figure on my own why Shit+Tab doesn’t work and add some % modulo to circle the options.

Now, the last piece and I’ll stop spamming your generous forum with my incompetence:
How do I move the menu to col X, row Y on the terminal?

I think you can just render everything to a win.child(opts) of your main window and play with opts.x_off and opts.y_off until you get what you want. Haven’t tested that though I’m pretty new to vaxis myself.

If you want to do anything more fancy than what you’re doing right now you might want to switch to vxfw.App as it handles lots of lower-level details for you and integrates nicely with the prebuilt widgets in the vxfw namespace.