Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/tui #160

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
18 changes: 11 additions & 7 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,18 @@ fn addZigupExe(
break :blk null;
};

const vaxis_dep = b.dependency("vaxis", .{
.target = target,
.optimize = optimize,
});

const exe = b.addExecutable(.{
.name = "zigup",
.root_source_file = b.path("zigup.zig"),
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("vaxis", vaxis_dep.module("vaxis"));

if (target.result.os.tag == .windows) {
exe.root_module.addImport("win32exelink", win32exelink_mod.?);
Expand Down Expand Up @@ -262,7 +268,7 @@ fn addTests(

tests.addWithClean(.{
.name = "test-bad-version",
.argv = &.{ "THIS_ZIG_VERSION_DOES_NOT_EXIT" },
.argv = &.{"THIS_ZIG_VERSION_DOES_NOT_EXIT"},
.checks = &.{
.{ .expect_stderr_match = "error: download '" },
.{ .expect_stderr_match = "' failed: " },
Expand All @@ -274,7 +280,7 @@ fn addTests(
// it should be more permanent
tests.addWithClean(.{
.name = "test-dev-version",
.argv = &.{ "0.14.0-dev.2465+70de2f3a7" },
.argv = &.{"0.14.0-dev.2465+70de2f3a7"},
.check = .{ .expect_stdout_exact = "" },
});

Expand Down Expand Up @@ -411,7 +417,7 @@ fn addTests(
tests.addWithClean(.{
.name = "test-default8-even-with-another-zig",
.env = default8,
.argv = &.{ "default" },
.argv = &.{"default"},
.check = .{ .expect_stdout_exact = "0.8.0\n" },
});
}
Expand Down Expand Up @@ -454,7 +460,6 @@ fn addTests(
.check = .{ .expect_stderr_exact = "error: compiler 'doesnotexist' does not exist, fetch it first with: zigup fetch doesnotexist\n" },
});


tests.addWithClean(.{
.name = "test-clean-default-master",
.env = master_7_and_8,
Expand Down Expand Up @@ -525,7 +530,7 @@ fn addTests(
tests.addWithClean(.{
.name = "test-clean-master",
.env = keep8_default_7,
.argv = &.{"clean", "master"},
.argv = &.{ "clean", "master" },
.checks = &.{
.{ .expect_stderr_match = "deleting '" },
.{ .expect_stderr_match = "master'\n" },
Expand Down Expand Up @@ -565,7 +570,6 @@ fn addTests(
},
});


tests.addWithClean(.{
.name = "test-clean8-as-default",
.env = default8,
Expand Down Expand Up @@ -618,7 +622,7 @@ const Tests = struct {
shared_options: SharedTestOptions,

fn addWithClean(tests: Tests, opt: TestOptions) void {
_ = tests.addCommon(opt, .yes_clean);
_ = tests.addCommon(opt, .yes_clean);
}
fn add(tests: Tests, opt: TestOptions) std.Build.LazyPath {
return tests.addCommon(opt, .no_clean);
Expand Down
7 changes: 6 additions & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
.{
.name = "zigup",
.version = "0.0.1",

.dependencies = .{
.vaxis = .{
.url = "https://github.com/rockorager/libvaxis/archive/refs/tags/v0.5.1.tar.gz",
.hash = "1220de23a3240e503397ea579de4fd85db422f537e10036ef74717c50164475813ce",
},
},
.paths = .{
"LICENSE",
"README.md",
Expand Down
129 changes: 129 additions & 0 deletions tui.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const std = @import("std");
const vaxis = @import("vaxis");
const Cell = vaxis.Cell;
const ScrollView = vaxis.widgets.ScrollView;
const Text = vaxis.vxfw.Text;
const log = std.log.scoped(.tui);

fn compareStrings(_: void, lhs: []const u8, rhs: []const u8) bool {
return std.mem.order(u8, lhs, rhs).compare(std.math.CompareOperator.gt);
}

pub const Command = union(enum) {
default: []const u8,
clean: []const u8,
exit: void,
};

const Event = union(enum) {
key_press: vaxis.Key,
winsize: vaxis.Winsize,
};

pub fn tui(allocator: std.mem.Allocator, compilers: [][]u8, default_compiler: ?[]const u8) !Command {
std.mem.sort([]const u8, compilers, {}, compareStrings);

var tty = try vaxis.Tty.init();
defer tty.deinit();

var vx = try vaxis.init(allocator, .{});
defer vx.deinit(allocator, tty.anyWriter());

var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
try loop.init();

try loop.start();
defer loop.stop();

try vx.enterAltScreen(tty.anyWriter());
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);

var selected_idx: u8 = 0;

var scroll_view: ScrollView = .{};

// The main event loop. Vaxis provides a thread safe, blocking, buffered
// queue which can serve as the primary event queue for an application
while (true) {
const event = loop.nextEvent();
const max_rows: usize = vx.window().height;
switch (event) {
.key_press => |key| {
if (key.matches(vaxis.Key.up, .{})) {
if (selected_idx > 0) {
selected_idx -= 1;
}
if (selected_idx < scroll_view.scroll.y and scroll_view.scroll.y > 0) {
scroll_view.scroll.y -= 1;
}
}
if (key.matches(vaxis.Key.down, .{})) {
if (selected_idx < compilers.len - 1) {
selected_idx += 1;
}
// account for borders and status bar
if (selected_idx >= max_rows - 3 and scroll_view.scroll.y < compilers.len - 1) {
scroll_view.scroll.y += 1;
}
}
if (key.codepoint == 'd') {
return Command{ .default = compilers[selected_idx] };
}
if (key.codepoint == 'c') {
return Command{ .clean = compilers[selected_idx] };
}
if (key.codepoint == 'q') {
break;
}
if (key.codepoint == 'c' and key.mods.ctrl) {
break;
}
},
.winsize => |ws| {
try vx.resize(allocator, tty.anyWriter(), ws);
},
}

const win = vx.window();
win.clear();

const child_bar = win.child(
.{
.y_off = win.height - 1,
.height = .{ .limit = 1 },
},
);

const child = win.child(
.{
.height = .{ .limit = win.height - 1 },
.border = .{ .where = .all, .glyphs = .single_square },
},
);

scroll_view.draw(child, .{ .cols = win.width, .rows = compilers.len });
_ = try child_bar.printSegment(.{ .text = "q:Quit d:Default c:Clean ↑:Up ↓:Down", .style = .{ .italic = true } }, .{ .wrap = .word, .commit = true });

for (compilers, 0..) |compiler, j| {
for (0..win.width - 3) |i| {
const color: Cell.Color = if (selected_idx == j) .{ .rgb = .{ 0x81, 0xA2, 0xBE } } else Cell.Color.default;

const currentSymbol: []const u8 = switch (i) {
0 => if (default_compiler) |def_comp| if (std.mem.eql(u8, def_comp, compiler)) "✓" else "o" else "o",
1 => " ",
else => if (i > compiler.len + 1) " " else compiler[i - 2 .. i - 1],
};

const cell: Cell = .{
.char = .{ .grapheme = currentSymbol },
.style = .{
.bg = color,
},
};
scroll_view.writeCell(child, @intCast(i), @intCast(j), cell);
}
}
try vx.render(tty.anyWriter());
}
return .exit;
}
114 changes: 77 additions & 37 deletions zigup.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const tui = @import("tui.zig");
const builtin = @import("builtin");
const mem = std.mem;

Expand Down Expand Up @@ -207,6 +208,7 @@ fn help() void {
\\ that aren't the default, master, or marked to keep.
\\ zigup keep VERSION mark a compiler to be kept during clean
\\ zigup run VERSION ARGS... run the given VERSION of the compiler with the given ARGS...
\\ zigup tui run the tui
\\
\\Uncommon Usage:
\\
Expand Down Expand Up @@ -285,6 +287,28 @@ pub fn main2() !u8 {
help();
return 1;
}
if (std.mem.eql(u8, "tui", args[0])) {
if (args.len != 1) {
std.log.err("'tui' command requires 0 arguments but got {d}", .{args.len - 1});
return 1;
}

while (true) {
const command = try runTui(allocator);
switch (command) {
.exit => return 0,
.default => |compiler| {
try setDefaultCompilerVersion(allocator, compiler);
std.debug.print("setting default compiler {s}\r\n", .{compiler});
},
.clean => |compiler| {
try cleanCompilers(allocator, compiler);
std.debug.print("cleaning compiler {s}\r\n", .{compiler});
},
}
}
return 0;
}
if (std.mem.eql(u8, "fetch-index", args[0])) {
if (args.len != 1) {
std.log.err("'index' command requires 0 arguments but got {d}", .{args.len - 1});
Expand Down Expand Up @@ -327,7 +351,7 @@ pub fn main2() !u8 {
std.log.err("'list' command requires 0 arguments but got {d}", .{args.len - 1});
return 1;
}
try listCompilers(allocator);
try printCompilers(allocator);
return 0;
}
if (std.mem.eql(u8, "default", args[0])) {
Expand All @@ -336,30 +360,7 @@ pub fn main2() !u8 {
return 0;
}
if (args.len == 2) {
const version_string = args[1];
const install_dir_string = try getInstallDir(allocator, .{ .create = true });
defer allocator.free(install_dir_string);
const resolved_version_string = init_resolved: {
if (!std.mem.eql(u8, version_string, "master"))
break :init_resolved version_string;

const optional_master_dir: ?[]const u8 = blk: {
var install_dir = std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }) catch |e| switch (e) {
error.FileNotFound => break :blk null,
else => return e,
};
defer install_dir.close();
break :blk try getMasterDir(allocator, &install_dir);
};
// no need to free master_dir, this is a short lived program
break :init_resolved optional_master_dir orelse {
std.log.err("master has not been fetched", .{});
return 1;
};
};
const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ install_dir_string, resolved_version_string });
defer allocator.free(compiler_dir);
try setDefaultCompiler(allocator, compiler_dir, .verify_existence);
try setDefaultCompilerVersion(allocator, args[1]);
return 0;
}
std.log.err("'default' command requires 1 or 2 arguments but got {d}", .{args.len - 1});
Expand All @@ -377,6 +378,12 @@ pub fn main2() !u8 {
//const optionalInstallPath = try find_zigs(allocator);
}

fn runTui(allocator: Allocator) !tui.Command {
const compilers = try listCompilers(allocator);
const default_compiler = try getDefaultCompiler(allocator);
return try tui.tui(allocator, compilers, default_compiler);
}

pub fn runCompiler(allocator: Allocator, args: []const []const u8) !u8 {
// disable log so we don't add extra output to whatever the compiler will output
global_enable_log = false;
Expand Down Expand Up @@ -558,25 +565,32 @@ fn existsAbsolute(absolutePath: []const u8) !bool {
return true;
}

fn listCompilers(allocator: Allocator) !void {
fn listCompilers(allocator: Allocator) ![][]u8 {
const install_dir_string = try getInstallDir(allocator, .{ .create = false });
defer allocator.free(install_dir_string);

var install_dir = std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }) catch |e| switch (e) {
error.FileNotFound => return,
else => return e,
};
var install_dir = try std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true });
defer install_dir.close();

var result = ArrayList([]u8).init(allocator);
defer result.deinit();
var it = install_dir.iterate();
while (try it.next()) |entry| {
if (entry.kind != .directory)
continue;
if (std.mem.endsWith(u8, entry.name, ".installing"))
continue;
try result.append(try allocator.dupe(u8, entry.name));
}
return result.toOwnedSlice() catch |e| oom(e);
}

fn printCompilers(allocator: Allocator) !void {
const compilers = try listCompilers(allocator);
const stdout = std.io.getStdOut().writer();
{
var it = install_dir.iterate();
while (try it.next()) |entry| {
if (entry.kind != .directory)
continue;
if (std.mem.endsWith(u8, entry.name, ".installing"))
continue;
try stdout.print("{s}\n", .{entry.name});
for (compilers) |compiler| {
try stdout.print("{s}\n", .{compiler});
}
}
}
Expand Down Expand Up @@ -722,6 +736,32 @@ fn printDefaultCompiler(allocator: Allocator) !void {

const ExistVerify = enum { existence_verified, verify_existence };

fn setDefaultCompilerVersion(allocator: Allocator, compiler_version: []const u8) !void {
const install_dir_string = try getInstallDir(allocator, .{ .create = true });
defer allocator.free(install_dir_string);
const resolved_version_string = init_resolved: {
if (!std.mem.eql(u8, compiler_version, "master"))
break :init_resolved compiler_version;

const optional_master_dir: ?[]const u8 = blk: {
var install_dir = std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }) catch |e| switch (e) {
error.FileNotFound => break :blk null,
else => return e,
};
defer install_dir.close();
break :blk try getMasterDir(allocator, &install_dir);
};
// no need to free master_dir, this is a short lived program
break :init_resolved optional_master_dir orelse {
std.log.err("master has not been fetched", .{});
std.process.exit(1);
};
};
const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ install_dir_string, resolved_version_string });
defer allocator.free(compiler_dir);
try setDefaultCompiler(allocator, compiler_dir, .verify_existence);
}

fn setDefaultCompiler(allocator: Allocator, compiler_dir: []const u8, exist_verify: ExistVerify) !void {
switch (exist_verify) {
.existence_verified => {},
Expand Down